diff --git a/.circleci/config.yml b/.circleci/config.yml
index 2a7a0e8..02e01d8 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -2,7 +2,7 @@ version: 2
 python_env: &python_env
   docker:
     - image: circleci/python:2.7-stretch-browsers
-    
+
 install_awscli: &install_awscli
   name: "Install awscli"
   command: |
@@ -14,7 +14,7 @@ install_deploysuite: &install_deploysuite
     cp ./../buildscript/master_deploy.sh .
     cp ./../buildscript/buildenv.sh .
     cp ./../buildscript/awsconfiguration.sh .
-            
+
 # Instructions of deployment
 deploy_steps: &deploy_steps
   - checkout
@@ -25,14 +25,14 @@ deploy_steps: &deploy_steps
   - setup_remote_docker
   - run: docker build -t ${APPNAME}:latest .
   - deploy:
-      name: "Running Masterscript -  deploy tc-project-service " 
+      name: "Running Masterscript -  deploy tc-project-service "
       command: |
-        ./awsconfiguration.sh $DEPLOY_ENV  
+        ./awsconfiguration.sh $DEPLOY_ENV
         source awsenvconf
         ./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-deployvar
         source buildenvvar
         ./master_deploy.sh -d ECS -e $DEPLOY_ENV -t latest -s ${LOGICAL_ENV}-global-appvar,${LOGICAL_ENV}-${APPNAME}-appvar -i ${APPNAME}
-       
+
 jobs:
   test:
     docker:
@@ -42,10 +42,11 @@ jobs:
         - POSTGRES_USER: circle_test
         - POSTGRES_DB: circle_test
       - image: elasticsearch:2.3
+      - image: rabbitmq:3-management
     environment:
       DEPLOY_ENV: "DEV"
       LOGICAL_ENV: "dev"
-      APPNAME: "projects-api"       
+      APPNAME: "projects-api"
     steps:
       - checkout
       - run:
@@ -53,10 +54,10 @@ jobs:
           command: |
               sudo apt update
               sudo apt install curl
-              sudo apt install python-pip               
+              sudo apt install python-pip
       - run: *install_awscli
       - run: *install_deploysuite
-      - setup_remote_docker      
+      - setup_remote_docker
       - restore_cache:
           key: test-node-modules-{{ checksum "package.json" }}
       - run: npm install
@@ -66,12 +67,12 @@ jobs:
             - node_modules
       - run: npm run lint
       - run:
-          name: "Running Masterscript -  deploy tc-project-service " 
+          name: "Running Masterscript -  deploy tc-project-service "
           command: |
-            ./awsconfiguration.sh $DEPLOY_ENV  
+            ./awsconfiguration.sh $DEPLOY_ENV
             source awsenvconf
             ./buildenv.sh -e $DEPLOY_ENV -b ${LOGICAL_ENV}-${APPNAME}-testvar
-            source buildenvvar         
+            source buildenvvar
             npm run test
             rm -f buildenvvar
       - run: npm run build
@@ -79,13 +80,13 @@ jobs:
           root: .
           paths:
             - dist
-            
+
   deployProd:
     <<: *python_env
     environment:
       DEPLOY_ENV: "PROD"
       LOGICAL_ENV: "prod"
-      APPNAME: "projects-api" 
+      APPNAME: "projects-api"
     steps: *deploy_steps
 
   deployDev:
@@ -93,9 +94,9 @@ jobs:
     environment:
       DEPLOY_ENV: "DEV"
       LOGICAL_ENV: "dev"
-      APPNAME: "projects-api" 
-    steps: *deploy_steps 
-    
+      APPNAME: "projects-api"
+    steps: *deploy_steps
+
 workflows:
   version: 2
   build:
diff --git a/.eslintrc b/.eslintrc
index b0115e3..00dbe70 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -8,7 +8,7 @@
     "mocha": true
   },
   "rules": {
-    "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "**/serviceMocks.js"]}],
+    "import/no-extraneous-dependencies": ["error", {"devDependencies": ["**/*.test.js", "**/*.spec.js", "src/tests/*.js"]}],
     "max-len": ["error", { "ignoreComments": true, "code": 120 }],
     "valid-jsdoc": ["error", {
       "requireReturn": true,
diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json
index 4889667..733bbbc 100644
--- a/config/custom-environment-variables.json
+++ b/config/custom-environment-variables.json
@@ -52,5 +52,17 @@
   "accountsAppUrl": "ACCOUNTS_APP_URL",
   "inviteEmailSubject": "INVITE_EMAIL_SUBJECT",
   "inviteEmailSectionTitle": "INVITE_EMAIL_SECTION_TITLE",
-  "pageSize": "PAGE_SIZE"
+  "pageSize": "PAGE_SIZE",
+  "SSO_REFCODES": "SSO_REFCODES",
+  "lookerConfig": {
+    "BASE_URL": "LOOKER_API_BASE_URL",
+    "CLIENT_ID": "LOOKER_API_CLIENT_ID",
+    "CLIENT_SECRET": "LOOKER_API_CLIENT_SECRET",
+    "TOKEN": "TOKEN",
+    "USE_MOCK": "LOOKER_API_ENABLE_MOCK",
+    "QUERIES": {
+      "REG_STATS": "LOOKER_API_REG_STATS_QUERY_ID",
+      "BUDGET": "LOOKER_API_BUDGET_QUERY_ID"
+    }
+  }
 }
diff --git a/config/default.json b/config/default.json
index 4b62d37..83446f6 100644
--- a/config/default.json
+++ b/config/default.json
@@ -56,5 +56,18 @@
   "accountsAppUrl": "https://accounts.topcoder-dev.com",
   "MAX_REVISION_NUMBER": 100,
   "UNIQUE_GMAIL_VALIDATION": false,
-  "pageSize": 20
+  "pageSize": 20,
+  "VALID_STATUSES_BEFORE_PAUSED": "[\"active\"]",
+  "SSO_REFCODES": "[]",
+  "lookerConfig": {
+    "BASE_URL": "",
+    "CLIENT_ID": "",
+    "CLIENT_SECRET": "",
+    "TOKEN": "TOKEN",
+    "USE_MOCK": "true",
+    "QUERIES": {
+      "REG_STATS": 1234,
+      "BUDGET": 123
+    }
+  }
 }
diff --git a/docs/Project API.postman_collection.json b/docs/Project API.postman_collection.json
index cb1e0c8..0c48429 100644
--- a/docs/Project API.postman_collection.json	
+++ b/docs/Project API.postman_collection.json	
@@ -1,6 +1,6 @@
 {
 	"info": {
-		"_postman_id": "90ff8f9b-5661-4642-8b8b-84aa4b581706",
+		"_postman_id": "dd9afefd-0b47-4483-a4e5-33ca395791a3",
 		"name": "Project API",
 		"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
 	},
@@ -244,6 +244,7 @@
 							"response": []
 						}
 					],
+					"protocolProfileBehavior": {},
 					"_postman_isSubFolder": true
 				},
 				{
@@ -435,7 +436,8 @@
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Project With TemplateId issue",
@@ -515,7 +517,8 @@
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Project Members",
@@ -1024,7 +1027,8 @@
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Project Members Invites",
@@ -1223,7 +1227,8 @@
 						]
 					}
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
 			"name": "Projects",
@@ -2157,15 +2162,16 @@
 					"response": []
 				}
 			],
-			"description": "Requests for all things projects."
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "EventHandling and Integration with Direct Project API",
+			"name": "Workstream",
 			"item": [
 				{
-					"name": "mock direct projects",
+					"name": "Create workstream without payload",
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Authorization",
@@ -2176,25 +2182,42 @@
 								"value": "application/json"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\n}"
+						},
 						"url": {
-							"raw": "https://api.topcoder-dev.com/v3/direct/projects",
-							"protocol": "https",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams",
 							"host": [
-								"api",
-								"topcoder-dev",
-								"com"
+								"{{api-url}}"
 							],
 							"path": [
-								"v3",
-								"direct",
-								"projects"
+								"projects",
+								"{{projectId}}",
+								"workstreams"
 							]
-						}
+						},
+						"description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code."
 					},
 					"response": []
 				},
 				{
-					"name": " Create direct project when a new project is successfully created",
+					"name": "Create workstream with valid values",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "15506f7a-77d3-46cb-9b37-41015ffbfdbc",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"workStreamId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
 						"method": "POST",
 						"header": [
@@ -2209,28 +2232,31 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n        \"type\": \"generic\",\n        \"description\": \"test project\",\n        \"details\": {},\n        \"billingAccountId\": 123,\n        \"name\": \"test project1\"\n      }"
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"type\": \"generic\",\n\t\t\"status\": \"active\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects"
+								"projects",
+								"{{projectId}}",
+								"workstreams"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Response error from direct project service",
+					"name": "Create workstream by inactive user",
 					"request": {
 						"method": "POST",
 						"header": [
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer userId_{{inactive-userId}}"
 							},
 							{
 								"key": "Content-Type",
@@ -2239,122 +2265,108 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\n        \"role\": \"copilot\"\n}"
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/members",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"members"
+								"workstreams"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": " Add co-pilot when a co-pilot is added to a project",
+					"name": "Get workstream by id",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\n\t\"role\": \"copilot\"\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/members",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"members"
+								"workstreams",
+								"{{workStreamId}}"
 							]
-						}
+						},
+						"description": "Get a project by id. project members and attachments should also be returned."
 					},
 					"response": []
 				},
 				{
-					"name": "remove copilot from direct project when editing project member role",
+					"name": "List workstreams",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": " {\n    \"role\": \"customer\",\n    \"isPrimary\": true\n } "
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"members",
-								"{{memberId}}"
+								"workstreams"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": " Sync billing account id with direct",
+					"name": "DELETE workstream by id",
 					"request": {
-						"method": "PATCH",
+						"method": "DELETE",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n        \"billingAccountId\": 9999, \n        \"name\": \"new project name\"\n      }"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"{{projectId}}"
+								"{{projectId}}",
+								"workstreams",
+								"{{workStreamId}}"
 							]
-						}
+						},
+						"description": "Delete a project by id"
 					},
 					"response": []
 				},
 				{
-					"name": "Delete co-pilot when a co-pilot is removed from  a project",
+					"name": "Update workstream",
 					"request": {
-						"method": "DELETE",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Authorization",
@@ -2367,67 +2379,33 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\t\t\"name\": \"test project - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"members",
-								"{{memberId}}"
+								"workstreams",
+								"{{workStreamId}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				}
 			],
-			"event": [
-				{
-					"listen": "prerequest",
-					"script": {
-						"id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67",
-						"type": "text/javascript",
-						"exec": [
-							""
-						]
-					}
-				},
-				{
-					"listen": "test",
-					"script": {
-						"id": "12f9d794-0872-4058-aafa-77b89e72025b",
-						"type": "text/javascript",
-						"exec": [
-							""
-						]
-					}
-				}
-			]
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Project Phase",
+			"name": "Work",
 			"item": [
 				{
-					"name": "Create Phase",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "7050133a-b934-4faf-8101-d2e80b5c0710",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Create work without payload",
 					"request": {
 						"method": "POST",
 						"header": [
@@ -2442,33 +2420,36 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}"
+							"raw": "{\n\t\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							]
-						}
+						},
+						"description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code."
 					},
 					"response": []
 				},
 				{
-					"name": "Create Phase with order",
+					"name": "Create work with valid values",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "2f771afe-7b4e-4260-b04d-324e880eb61b",
+								"id": "34d06fac-76f6-47b8-9b70-6e9c558e2bf1",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
+									"    pm.environment.set(\"workId\", pm.response.json().id);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -2489,45 +2470,33 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n\t\"order\": 1\n}"
+							"raw": "{\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-16T00:00:00\",\n\t\t\"endDate\": \"2018-05-17T00:00:00\",\n\t\t\"order\": 1,\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Create Phase with productTemplateId",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "8415ad98-b3f6-4330-88b6-e1830da2e4f9",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Create work - 403",
 					"request": {
 						"method": "POST",
 						"header": [
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token-member-40051331}}"
 							},
 							{
 								"key": "Content-Type",
@@ -2536,216 +2505,215 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n\t\"order\": 1,\n\t\"productTemplateId\": {{productTemplateId}}\n}"
+							"raw": "{\n\t\t\"name\": \"test project phase\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"order\": 2,\n\t\t\"budget\": 20,\n\t\t\"details\": {\n\t\t\t\"aDetails\": \"a details\"\n\t\t}\n\t}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "List Phase",
+					"name": "Create work by inactive user",
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer userId_{{inactive-userId}}"
 							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n}"
+						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "List Phase with fields",
+					"name": "Get work by id",
 					"request": {
 						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
-							],
-							"query": [
-								{
-									"key": "fields",
-									"value": "status,name,budget"
-								}
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}"
 							]
-						}
+						},
+						"description": "Get a project by id. project members and attachments should also be returned."
 					},
 					"response": []
 				},
 				{
-					"name": "List Phase with sort",
+					"name": "List works",
 					"request": {
 						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases?sort=status desc",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
-							],
-							"query": [
-								{
-									"key": "sort",
-									"value": "status desc"
-								}
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "List Phase with sort by order",
+					"name": "List works - sort",
 					"request": {
 						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases?sort=order desc",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?sort=startDate desc",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases"
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
 							],
 							"query": [
 								{
 									"key": "sort",
-									"value": "order desc"
+									"value": "startDate desc"
 								}
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Get Phase",
+					"name": "List works - fields",
 					"request": {
 						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works?fields=status,name,budget",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}"
-							]
-						}
+								"workstreams",
+								"{{workStreamId}}",
+								"works"
+							],
+							"query": [
+								{
+									"key": "fields",
+									"value": "status,name,budget"
+								}
+							]
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Update Phase",
+					"name": "DELETE work by id",
 					"request": {
-						"method": "PATCH",
+						"method": "DELETE",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t}\n}"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}"
 							]
-						}
+						},
+						"description": "Delete a project by id"
 					},
 					"response": []
 				},
 				{
-					"name": "Update Phase with order",
+					"name": "Update work",
 					"request": {
 						"method": "PATCH",
 						"header": [
@@ -2760,31 +2728,34 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n\t\"order\": 1\n}"
+							"raw": "{\n\t\t\"name\": \"test project phase 11\",\n\t\t\"status\": \"active\",\n\t\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\t\"budget\": 24,\n\t\t\"order\": 2\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Delete Phase",
+					"name": "Update work 403",
 					"request": {
-						"method": "DELETE",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
 							},
 							{
 								"key": "Content-Type",
@@ -2793,42 +2764,80 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\t\t\"name\": \"test project - updated\",\n\t\t\"type\": \"generic\",\n\t\t\"status\": \"active\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}"
+								"workstreams",
+								"{{workStreamId}}"
 							]
-						}
+						},
+						"description": "Update the project name. If user don't have permission to the project than it should return 403."
 					},
 					"response": []
 				}
-			]
+			],
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Phase Products",
+			"name": "Work Item",
 			"item": [
 				{
-					"name": "Create Phase Product",
+					"name": "Create work item without payload",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/workstreams/{{workStreamId}}/works/{{workId}}/workitems",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems"
+							]
+						},
+						"description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code."
+					},
+					"response": []
+				},
+				{
+					"name": "Create work item with valid values",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "77f089b3-cbe6-4fb4-b54f-2a52d138a050",
+								"type": "text/javascript",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"phaseProductId\", pm.response.json().id);",
+									"    pm.environment.set(\"itemId\", pm.response.json().id);",
 									"});"
-								],
-								"type": "text/javascript"
+								]
 							}
 						}
 					],
@@ -2846,26 +2855,66 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test phase product\",\n\t\"type\": \"type 1\",\n\t\"estimatedPrice\": 10\n}"
+							"raw": "{\n\t\t\"name\": \"work item - phase product\",\n\t\t\"type\": \"type 1\",\n\t\t\"estimatedPrice\": 12\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}",
-								"products"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "List Phase Products",
+					"name": "Create work item by inactive user",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer userId_{{inactive-userId}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"description\": \"Hello I am a test project\",\n\t\t\"type\": \"generic\"\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"{{projectId}}",
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems"
+							]
+						},
+						"description": "Valid request body. Project should be created successfully."
+					},
+					"response": []
+				},
+				{
+					"name": "Get work item by id",
 					"request": {
 						"method": "GET",
 						"header": [
@@ -2875,23 +2924,27 @@
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems/{{itemId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}",
-								"products"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems",
+								"{{itemId}}"
 							]
-						}
+						},
+						"description": "Get a project by id. project members and attachments should also be returned."
 					},
 					"response": []
 				},
 				{
-					"name": "Get Phase Product",
+					"name": "List work items",
 					"request": {
 						"method": "GET",
 						"header": [
@@ -2901,151 +2954,147 @@
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}",
-								"products",
-								"{{phaseProductId}}"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Update Phase Product",
+					"name": "DELETE work item by id",
 					"request": {
-						"method": "PATCH",
+						"method": "DELETE",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							},
-							{
-								"key": "Content-Type",
-								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems/{{itemId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}",
-								"products",
-								"{{phaseProductId}}"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems",
+								"{{itemId}}"
 							]
-						}
+						},
+						"description": "Delete a project by id"
 					},
 					"response": []
 				},
 				{
-					"name": "Delete Phase Product",
+					"name": "Update work item",
 					"request": {
-						"method": "DELETE",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\t\t\"name\": \"work item - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId}}/works/{{workId}}/workitems/{{itemId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"phases",
-								"{{phaseId}}",
-								"products",
-								"{{phaseProductId}}"
+								"workstreams",
+								"{{workStreamId}}",
+								"works",
+								"{{workId}}",
+								"workitems",
+								"{{itemId}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				}
-			]
+			],
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Project Templates",
+			"name": "Work Management Permission",
 			"item": [
 				{
-					"name": "Create project template",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "2f79c07b-8076-4715-abf7-1d6903df444f",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Create work management permission without payload",
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\"\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\"\r\n    }\r\n  }"
+							"raw": "{\n\t\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission"
 							]
-						}
+						},
+						"description": "Request body is mandatory while creating project. If invalid request body is supplied this should return 422 status code."
 					},
 					"response": []
 				},
 				{
-					"name": "Create project template with form, priceConfig, planConfig",
+					"name": "Create work management permission with valid values 1",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "4c442ea3-0834-4a30-8044-a4e94fd4ea2d",
+								"id": "305f3d68-6e9f-4c4d-b031-7487986a93e2",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
+									"    pm.environment.set(\"workManagementPermissionId\", pm.response.json().id);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -3055,44 +3104,45 @@
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    }\r\n  }"
+							"raw": "{\n      \"policy\": \"work.new.create\",\n      \"permission\": {\n        \"allowRule\": {\n          \"projectRoles\": [\"customer\", \"copilot\"],\n          \"topcoderRoles\": [\"Connect Manager\", \"Connect Admin\", \"administrator\"]\n        },\n        \"denyRule\": { \"projectRoles\": [\"copilot\"] }\n      },\n      \"projectTemplateId\": 1\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Create project template with only form key",
+					"name": "Create work management permission with valid values 2",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "7d0ae3ca-fe2d-40eb-b5c8-9b03955babec",
+								"id": "305f3d68-6e9f-4c4d-b031-7487986a93e2",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
+									"    pm.environment.set(\"workManagementPermissionId\", pm.response.json().id);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -3102,265 +3152,250 @@
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\"\r\n    }\r\n  }"
+							"raw": "{\n      \"policy\": \"work.new.create\",\n      \"permission\": {\n        \"allowRule\": {\n          \"projectRoles\": [\"customer\", \"copilot\"],\n          \"topcoderRoles\": [\"Connect Manager\", \"Connect Admin\", \"administrator\"]\n        },\n        \"denyRule\": { \"projectRoles\": [\"copilot\"] }\n      },\n      \"projectTemplateId\": 2\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Create project template with wrong form key",
+					"name": "Create work management permission by inactive user",
 					"request": {
 						"method": "POST",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer userId_{{inactive-userId}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"wrong-key\"\r\n    }\r\n  }"
+							"raw": "{\n      \"policy\": \"work.create\",\n      \"permission\": {\n        \"allowRule\": {\n          \"projectRoles\": [\"customer\", \"copilot\"],\n          \"topcoderRoles\": [\"Connect Manager\", \"Connect Admin\", \"administrator\"]\n        },\n        \"denyRule\": { \"projectRoles\": [\"copilot\"] }\n      },\n      \"projectTemplateId\": 1\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Create project template with wrong model version",
+					"name": "Get work management permission by id",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    }\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission/{{workManagementPermissionId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission",
+								"{{workManagementPermissionId}}"
 							]
-						}
+						},
+						"description": "Get a project by id. project members and attachments should also be returned."
 					},
 					"response": []
 				},
 				{
-					"name": "List project templates",
+					"name": "List work management permissions - filter required",
 					"request": {
 						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates"
+								"workManagementPermission"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Get project template",
+					"name": "List work management permissions - missing filter",
 					"request": {
 						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=template",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}"
+								"workManagementPermission"
+							],
+							"query": [
+								{
+									"key": "filter",
+									"value": "template"
+								}
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a project template with from, priceConfig, planConfig",
+					"name": "List work management permissions - invalid filter",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 2\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"qa\",\t\r\n    \t \"version\": 2\t\r\n    }\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=invalid%3D2%26projectTemplateId%3D1",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}",
-								"upgrade"
+								"workManagementPermission"
+							],
+							"query": [
+								{
+									"key": "filter",
+									"value": "invalid%3D2%26projectTemplateId%3D1"
+								}
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a project template with wrong model version",
+					"name": "List work management permissions",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    }\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}",
-								"upgrade"
+								"workManagementPermission"
+							],
+							"query": [
+								{
+									"key": "filter",
+									"value": "projectTemplateId%3D1"
+								}
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a project template without define form, priceConfig, planConfig",
+					"name": "Update work management permission",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n}"
+							"raw": "{\n      \"policy\": \"work.new.delete\",\n      \"permission\": {\n        \"allowRule\": {\n          \"projectRoles\": [\"customer\", \"copilot\"],\n          \"topcoderRoles\": [\"Connect Manager\", \"Connect Admin\", \"administrator\"]\n        },\n        \"denyRule\": { \"projectRoles\": [\"copilot\"] }\n      },\n      \"projectTemplateId\": 1\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission/{{workManagementPermissionId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}",
-								"upgrade"
+								"workManagementPermission",
+								"{{workManagementPermissionId}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Update project template",
+					"name": "Delete work management permission by id",
 					"request": {
-						"method": "PATCH",
+						"method": "DELETE",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
@@ -3368,882 +3403,769 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/workManagementPermission/{{workManagementPermissionId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}"
+								"workManagementPermission",
+								"{{workManagementPermissionId}}"
 							]
-						}
+						},
+						"description": "Delete a project by id"
 					},
 					"response": []
-				},
+				}
+			],
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Permissions",
+			"item": [
 				{
-					"name": "Update project template with define form, priceConfig, planConfig",
+					"name": "Get permissions - 404",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"form\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    },\r\n    \"priceConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    }\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
+							"raw": "{{api-url}}/projects/9999/permissions",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}"
+								"9999",
+								"permissions"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Update project template with wrong form, priceConfig, planConfig",
+					"name": "Create project invite for customer with valid values",
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n\t\"form\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    },\r\n    \"priceConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    },\r\n    \"planConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    }\r\n  }"
+							"raw": "{\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"customer\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/invite",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}"
+								"{{projectId}}",
+								"members",
+								"invite"
 							]
-						}
+						},
+						"description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project."
 					},
 					"response": []
 				},
 				{
-					"name": "Delete project template",
+					"name": "Update project invite to accept invite",
 					"request": {
-						"method": "DELETE",
+						"method": "PUT",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-member-40051331}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"new category\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
+							"raw": "{\n\t\t\"userId\": 40051331,\n\t\t\"status\": \"accepted\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/invite",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"projectTemplates",
-								"{{projectTemplateId}}"
+								"{{projectId}}",
+								"members",
+								"invite"
 							]
-						}
+						},
+						"description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project."
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Product Templates",
-			"item": [
+				},
 				{
-					"name": "Create product template",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "b5aaf185-6026-4b58-b9b8-56616109cd5a",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"productTemplateId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Get permissions",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"{{productCategoryId}}\",\r\n    \"subCategory\": \"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"template\": {\r\n      \"template1\": {\r\n        \"name\": \"template 1\",\r\n        \"details\": {\r\n          \"anyDetails\": \"any details 1\"\r\n        },\r\n        \"others\": [\"others 11\", \"others 12\"]\r\n      },\r\n      \"template2\": {\r\n        \"name\": \"template 2\",\r\n        \"details\": {\r\n          \"anyDetails\": \"any details 2\"\r\n        },\r\n        \"others\": [\"others 21\", \"others 22\"]\r\n      }\r\n    }\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates",
+							"raw": "{{api-url}}/projects/{{projectId}}/permissions",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates"
+								"{{projectId}}",
+								"permissions"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
 				},
 				{
-					"name": "Create product template with form",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "d5a2af2e-97d2-415c-a533-1d52dd4003c7",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"productTemplateId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Get permissions - manager",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token-manager-40051334}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"dev\",\r\n\t\t\"version\": 1\r\n\t}\r\n  }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates",
+							"raw": "{{api-url}}/projects/{{projectId}}/permissions",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates"
+								"{{projectId}}",
+								"permissions"
 							]
-						}
+						},
+						"description": "List all the project with no filter. Default sort and limits are applied."
 					},
 					"response": []
-				},
-				{
-					"name": "Create product template with wrong form key",
+				}
+			],
+			"event": [
+				{
+					"listen": "prerequest",
+					"script": {
+						"id": "5197d1ec-429c-4a6f-9e9c-3ec3cd6f292a",
+						"type": "text/javascript",
+						"exec": [
+							""
+						]
+					}
+				},
+				{
+					"listen": "test",
+					"script": {
+						"id": "cc0cbbf1-54d1-481f-b8fa-a6dc4c80e993",
+						"type": "text/javascript",
+						"exec": [
+							""
+						]
+					}
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "WorkManagementForTemplate",
+			"item": [
+				{
+					"name": "Create workstream with valid values",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"type": "text/javascript",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"workStreamId1\", pm.response.json().id);",
+									"});"
+								]
+							}
+						}
+					],
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"wrong-key\"\r\n\t}\r\n  }"
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"type\": \"generic\",\n\t\t\"status\": \"active\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates"
+								"{{projectId}}",
+								"workstreams"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Create product template with wrong model version",
+					"name": "Create project invite for customer with valid values",
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"dev\",\r\n\t\t\"version\": 1123\r\n\t}\r\n  }"
+							"raw": "{\n\t\t\"userIds\": [40051331],\n\t\t\"role\": \"customer\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/invite",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates"
+								"{{projectId}}",
+								"members",
+								"invite"
 							]
-						}
+						},
+						"description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project."
 					},
 					"response": []
 				},
 				{
-					"name": "List product templates",
+					"name": "Update project invite to accept invite",
 					"request": {
-						"method": "GET",
+						"method": "PUT",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-member-40051331}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\t\"userId\": 40051331,\n\t\t\"status\": \"accepted\"\n}"
+						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/invite",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates"
+								"{{projectId}}",
+								"members",
+								"invite"
 							]
-						}
+						},
+						"description": "If the request payload is valid, than project customer should be added. This should sync with the direct project is project is associated with direct project."
 					},
 					"response": []
 				},
 				{
-					"name": "Get product template",
+					"name": "Update work stream - no match allowRule",
 					"request": {
-						"method": "GET",
+						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\t\"name\": \"work item - updated\"\n}"
+						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId1}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}"
+								"{{projectId}}",
+								"workstreams",
+								"{{workStreamId1}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Update product template",
+					"name": "Update work stream - allow access",
 					"request": {
 						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"productKey\":\"new productKey\",\r\n    \"category\":\"key1\",\r\n    \"icon\":\"http://example.com/icon-new.ico\",\r\n    \"brief\": \"new brief\",\r\n    \"details\": \"new details\",\r\n    \"aliases\":{\r\n      \"alias1\":\"scope 1\",\r\n      \"alias2\": [\"a\"]\r\n    },\r\n    \"template\":{\r\n      \"template1\":\"template 1\",\r\n      \"template2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
+							"raw": "{\n\t\t\"name\": \"work item - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId1}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}"
+								"{{projectId}}",
+								"workstreams",
+								"{{workStreamId1}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Delete product template",
+					"name": "Update work stream - allow access with ProjectRoles",
 					"request": {
-						"method": "DELETE",
+						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-member-40051331}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\t\t\"name\": \"work item - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/workstreams/{{workStreamId1}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}"
+								"{{projectId}}",
+								"workstreams",
+								"{{workStreamId1}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a product template with form",
+					"name": "Create workstream with valid values - Project 2",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"type": "text/javascript",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"workStreamId2\", pm.response.json().id);",
+									"});"
+								]
+							}
+						}
+					],
 					"request": {
 						"method": "POST",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}",
-								"type": "text"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t\"version\": 2\r\n    }\r\n  }"
+							"raw": "{\n\t\t\"name\": \"test project\",\n\t\t\"type\": \"generic\",\n\t\t\"status\": \"active\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/2/workstreams",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}",
-								"upgrade"
+								"2",
+								"workstreams"
 							]
-						}
+						},
+						"description": "Valid request body. Project should be created successfully."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a product template with wrong model version",
+					"name": "Update work item - allow access",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-manager-40051334}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}",
-								"type": "text"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t\"version\": 1234\r\n    }\r\n  }"
+							"raw": "{\n\t\t\"name\": \"test project - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/2/workstreams/{{workStreamId2}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}",
-								"upgrade"
+								"2",
+								"workstreams",
+								"{{workStreamId2}}"
 							]
-						}
+						},
+						"description": "Update the project name. If user don't have permission to the project than it should return 403."
 					},
 					"response": []
 				},
 				{
-					"name": "Upgrade a product template without define form",
+					"name": "Update workstream - deny access",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}",
-								"type": "text"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n  \r\n}"
+							"raw": "{\n\t\t\"name\": \"work item - updated\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
+							"raw": "{{api-url}}/projects/2/workstreams/{{workStreamId2}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productTemplates",
-								"{{productTemplateId}}",
-								"upgrade"
+								"2",
+								"workstreams",
+								"{{workStreamId2}}"
 							]
-						}
+						},
+						"description": "Update the project name. Name should be updated successfully."
 					},
 					"response": []
 				}
-			]
+			],
+			"description": "Requests for all things projects.",
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Project Type",
+			"name": "EventHandling and Integration with Direct Project API",
 			"item": [
 				{
-					"name": "Create project type",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"projectTypeId\", pm.response.json().key);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "mock direct projects",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							}
-						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n        \"key\": \"new key\",\r\n        \"displayName\": \"new displayName\",\r\n        \"icon\": \"http://example.com/icon4.ico\",\r\n    \t\"question\": \"question 4\",\r\n    \t\"info\": \"info 4\",\r\n    \t\"aliases\": [\"key-41\", \"key_42\"],\r\n    \t\"metadata\": {}\r\n  }"
-						},
-						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTypes",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"projects",
-								"metadata",
-								"projectTypes"
-							]
-						}
-					},
-					"response": []
-				},
-				{
-					"name": "List project types",
-					"request": {
-						"method": "GET",
-						"header": [
+							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTypes",
+							"raw": "https://api.topcoder-dev.com/v3/direct/projects",
+							"protocol": "https",
 							"host": [
-								"{{api-url}}"
+								"api",
+								"topcoder-dev",
+								"com"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"projectTypes"
+								"v3",
+								"direct",
+								"projects"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get project type",
+					"name": " Create direct project when a new project is successfully created",
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							}
-						],
-						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"projects",
-								"metadata",
-								"projectTypes",
-								"{{projectTypeId}}"
-							]
-						}
-					},
-					"response": []
-				},
-				{
-					"name": "Update project type",
-					"request": {
-						"method": "PATCH",
-						"header": [
+							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"displayName\": \"Chatbot-updated\"\r\n  }"
+							"raw": "{\n        \"type\": \"generic\",\n        \"description\": \"test project\",\n        \"details\": {},\n        \"billingAccountId\": 123,\n        \"name\": \"test project1\"\n      }"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
+							"raw": "{{api-url}}/projects",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"projectTypes",
-								"{{projectTypeId}}"
+								"projects"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete project type",
+					"name": "Response error from direct project service",
 					"request": {
-						"method": "DELETE",
+						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\n        \"role\": \"copilot\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/members",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"projectTypes",
-								"{{projectTypeId}}"
+								"{{projectId}}",
+								"members"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Product Category",
-			"item": [
+				},
 				{
-					"name": "Create product category",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "06156797-ceb2-4f8c-9448-5c453adb7b7a",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"productCategoryId\", pm.response.json().key);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": " Add co-pilot when a co-pilot is added to a project",
 					"request": {
 						"method": "POST",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n        \"key\": \"generic\",\r\n        \"displayName\": \"new displayName\",\r\n        \"icon\": \"icon\",\r\n        \"question\": \"question\",\r\n        \"info\": \"info\",\r\n        \"aliases\": [\"key-1\", \"key-2\"]\r\n  }"
+							"raw": "{\n\t\"role\": \"copilot\"\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productCategories",
+							"raw": "{{api-url}}/projects/{{projectId}}/members",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productCategories"
+								"{{projectId}}",
+								"members"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List product categories",
+					"name": "remove copilot from direct project when editing project member role",
 					"request": {
-						"method": "GET",
+						"method": "PATCH",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							}
-						],
-						"url": {
-							"raw": "{{api-url}}/projects/metadata/productCategories",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"projects",
-								"metadata",
-								"productCategories"
-							]
-						}
-					},
-					"response": []
-				},
-				{
-					"name": "Get product category",
-					"request": {
-						"method": "GET",
-						"header": [
+							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": " {\n    \"role\": \"customer\",\n    \"isPrimary\": true\n } "
+						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productCategories",
-								"{{productCategoryId}}"
+								"{{projectId}}",
+								"members",
+								"{{memberId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update product category",
+					"name": " Sync billing account id with direct",
 					"request": {
 						"method": "PATCH",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"displayName\": \"Chatbot-updated\"\r\n  }"
+							"raw": "{\n        \"billingAccountId\": 9999, \n        \"name\": \"new project name\"\n      }"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productCategories",
-								"{{productCategoryId}}"
+								"{{projectId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete product category",
+					"name": "Delete co-pilot when a co-pilot is removed from  a project",
 					"request": {
 						"method": "DELETE",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"new category\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/members/{{memberId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"productCategories",
-								"{{productCategoryId}}"
+								"{{projectId}}",
+								"members",
+								"{{memberId}}"
 							]
 						}
 					},
 					"response": []
 				}
 			],
-			"auth": {
-				"type": "bearer",
-				"bearer": [
-					{
-						"key": "token",
-						"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwc2hhaDEiLCJleHAiOjI0NjI0OTQ2MTgsInVzZXJJZCI6IjQwMTM1OTc4IiwiaWF0IjoxNDYyNDk0MDE4LCJlbWFpbCI6InBzaGFoMUB0ZXN0LmNvbSIsImp0aSI6ImY0ZTFhNTE0LTg5ODAtNDY0MC04ZWM1LWUzNmUzMWE3ZTg0OSJ9.XuNN7tpMOXvBG1QwWRQROj7NfuUbqhkjwn39Vy4tR5I",
-						"type": "string"
-					}
-				]
-			},
 			"event": [
 				{
 					"listen": "prerequest",
 					"script": {
-						"id": "f0092ef5-e624-4c25-87b2-b6a9e4c81ec8",
+						"id": "ef96ac6a-0fc0-4a64-a4fe-5390e17afe67",
 						"type": "text/javascript",
 						"exec": [
 							""
@@ -4253,25 +4175,41 @@
 				{
 					"listen": "test",
 					"script": {
-						"id": "9183c429-a5e0-4bf9-96a2-89f4d66e9b0d",
+						"id": "12f9d794-0872-4058-aafa-77b89e72025b",
 						"type": "text/javascript",
 						"exec": [
 							""
 						]
 					}
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Project upgrade",
+			"name": "Project Phase",
 			"item": [
 				{
-					"name": "Migrate project",
-					"request": {
-						"method": "POST",
-						"header": [
-							{
-								"key": "Authorization",
+					"name": "Create Phase",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7050133a-b934-4faf-8101-d2e80b5c0710",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							},
 							{
@@ -4281,24 +4219,39 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}"
+							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t}\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"upgrade"
+								"phases"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Migrate project (completed)",
+					"name": "Create Phase with order",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "2f771afe-7b4e-4260-b04d-324e880eb61b",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
 						"method": "POST",
 						"header": [
@@ -4313,24 +4266,39 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}"
+							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n\t\"order\": 1\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"upgrade"
+								"phases"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Migrate project with phase name",
+					"name": "Create Phase with productTemplateId",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "8415ad98-b3f6-4330-88b6-e1830da2e4f9",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"phaseId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
 						"method": "POST",
 						"header": [
@@ -4345,26 +4313,26 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}"
+							"raw": "{\n\t\"name\": \"test project phase\",\n\t\"status\": \"active\",\n\t\"startDate\": \"2018-05-15T00:00:00\",\n\t\"endDate\": \"2018-05-16T00:00:00\",\n\t\"budget\": 20,\n\t\"details\": {\n\t\t\"aDetails\": \"a details\"\n\t},\n\t\"order\": 1,\n\t\"productTemplateId\": {{productTemplateId}}\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"upgrade"
+								"phases"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Migrate project with phase name (completed)",
+					"name": "List Phase",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Authorization",
@@ -4375,186 +4343,116 @@
 								"value": "application/json"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"{{projectId}}",
-								"upgrade"
+								"phases"
 							]
 						}
 					},
 					"response": []
-				}
-			],
-			"description": "Request to migrate projects."
-		},
-		{
-			"name": "Timeline",
-			"item": [
+				},
 				{
-					"name": "Create timeline",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "c066e7d4-537f-406e-a768-ec4bf73a2634",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"timelineId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "List Phase with fields",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
-							}
-						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n\t\"name\":\"new name\",\r\n\t\"description\":\"new description\",\r\n\t\"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n\t\"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n\t\"reference\": \"project\",\r\n\t\"referenceId\": {{projectId}}\r\n}"
-						},
-						"url": {
-							"raw": "{{api-url}}/timelines",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"timelines"
-							]
-						}
-					},
-					"response": []
-				},
-				{
-					"name": "Create timeline with templateId",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "ee729ed9-0072-4821-9141-3615ff66f728",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"timelineId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
-					"request": {
-						"method": "POST",
-						"header": [
+								"value": "Bearer {{jwt-token}}"
+							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"description\":\"new description\",\r\n    \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1,\r\n    \"templateId\": 1\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases?fields=status,name,budget",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines"
+								"projects",
+								"{{projectId}}",
+								"phases"
+							],
+							"query": [
+								{
+									"key": "fields",
+									"value": "status,name,budget"
+								}
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create timeline with invalid data",
+					"name": "List Phase with sort",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-28T00:00:00.000Z\",\r\n    \"reference\": \"invalid\",\r\n    \"referenceId\": 0\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases?sort=status desc",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines"
+								"projects",
+								"{{projectId}}",
+								"phases"
+							],
+							"query": [
+								{
+									"key": "sort",
+									"value": "status desc"
+								}
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List timelines (filter by reference and referenceId)",
+					"name": "List Phase with sort by order",
 					"request": {
 						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-copilot-40051332}}",
-								"disabled": true
+								"value": "Bearer {{jwt-token}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}",
-								"type": "text"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/timelines?reference=project&referenceId={{projectId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases?sort=order desc",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines"
+								"projects",
+								"{{projectId}}",
+								"phases"
 							],
 							"query": [
 								{
-									"key": "reference",
-									"value": "project"
-								},
-								{
-									"key": "referenceId",
-									"value": "{{projectId}}"
+									"key": "sort",
+									"value": "order desc"
 								}
 							]
 						}
@@ -4562,137 +4460,112 @@
 					"response": []
 				},
 				{
-					"name": "Get timeline",
+					"name": "Get Phase",
 					"request": {
 						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update timeline",
+					"name": "Update Phase",
 					"request": {
 						"method": "PATCH",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
-							}
-						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-01T00:00:00.000Z\",\r\n    \"endDate\": null,\r\n    \"reference\": \"project\",\r\n    \"referenceId\": {{projectId}}\r\n}"
-						},
-						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"timelines",
-								"{{timelineId}}"
-							]
-						}
-					},
-					"response": []
-				},
-				{
-					"name": "Update timeline (startDate)",
-					"request": {
-						"method": "PATCH",
-						"header": [
+							},
 							{
 								"key": "Content-Type",
 								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1\r\n}"
+							"raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t}\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update timeline (endDate)",
+					"name": "Update Phase with order",
 					"request": {
 						"method": "PATCH",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1\r\n}"
+							"raw": "{\n\t\"name\": \"test project phase xxx\",\n\t\"status\": \"inactive\",\n\t\"startDate\": \"2018-05-14T00:00:00\",\n\t\"endDate\": \"2018-05-15T00:00:00\",\n\t\"budget\": 30,\n\t\"progress\": 15,\n\t\"details\": {\n\t\t\"message\": \"phase details\"\n\t},\n\t\"order\": 1\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete timeline",
+					"name": "Delete Phase",
 					"request": {
 						"method": "DELETE",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
@@ -4700,34 +4573,37 @@
 							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}"
 							]
 						}
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Milestone",
+			"name": "Phase Products",
 			"item": [
 				{
-					"name": "Create milestone",
+					"name": "Create Phase Product",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069",
+								"id": "77f089b3-cbe6-4fb4-b54f-2a52d138a050",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"milestoneId\", pm.response.json().id);",
+									"    pm.environment.set(\"phaseProductId\", pm.response.json().id);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -4738,160 +4614,178 @@
 						"method": "POST",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 3\",\r\n    \"description\": \"description 3\",\r\n    \"duration\": 4,\r\n    \"startDate\": \"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n    \"completionDate\": \"2018-05-31T00:00:00.000Z\",\r\n    \"status\": \"open\",\r\n    \"type\": \"type3\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            2,\r\n            3,\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 3\",\r\n    \"activeText\": \"activeText 3\",\r\n    \"completedText\": \"completedText 3\",\r\n    \"blockedText\": \"blockedText 3\"\r\n}"
+							"raw": "{\n\t\"name\": \"test phase product\",\n\t\"type\": \"type 1\",\n\t\"estimatedPrice\": 10\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}",
+								"products"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create milestone with invalid data",
+					"name": "List Phase Products",
 					"request": {
-						"method": "POST",
+						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-member-40051331}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-04T00:00:00.000Z\",\r\n    \"completionDate\": \"2018-05-04T00:00:00.000Z\"\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}",
+								"products"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List milestones",
+					"name": "Get Phase Product",
 					"request": {
 						"method": "GET",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}",
-								"type": "text"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}",
+								"products",
+								"{{phaseProductId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List milestones (sort)",
+					"name": "Update Phase Product",
 					"request": {
-						"method": "GET",
+						"method": "PATCH",
 						"header": [
 							{
-								"key": "Content-Type",
-								"value": "application/json"
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							},
 							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token-copilot-40051332}}"
+								"key": "Content-Type",
+								"value": "application/json"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\"name\": \"test phase product xxx\",\n\t\"type\": \"type 2\",\n\t\"templateId\": 10,\n\t\"estimatedPrice\": 1.234567,\n\t\"actualPrice\": 2.34567,\n\t\"details\": {\n\t\t\"message\": \"this is a JSON type. You can use any json\"\n\t}\n}"
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones?sort=order desc",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones"
-							],
-							"query": [
-								{
-									"key": "sort",
-									"value": "order desc"
-								}
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}",
+								"products",
+								"{{phaseProductId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get milestone",
+					"name": "Delete Phase Product",
 					"request": {
-						"method": "GET",
+						"method": "DELETE",
 						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
 							{
 								"key": "Authorization",
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/phases/{{phaseId}}/products/{{phaseProductId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"{{projectId}}",
+								"phases",
+								"{{phaseId}}",
+								"products",
+								"{{phaseProductId}}"
 							]
 						}
 					},
 					"response": []
-				},
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Project Templates",
+			"item": [
 				{
-					"name": "Update milestone",
+					"name": "Create project template",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "2f79c07b-8076-4715-abf7-1d6903df444f",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -4904,60 +4798,88 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-09-28T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\"\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\"\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (active)",
-					"request": {
-						"method": "PATCH",
-						"header": [
-							{
-								"key": "Content-Type",
-								"value": "application/json"
-							},
-							{
-								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
-							}
+					"name": "Create project template with form, priceConfig, planConfig",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "4c442ea3-0834-4a30-8044-a4e94fd4ea2d",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 2-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-10-28T00:00:00.000Z\",\r\n    \"status\": \"active\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (completed)",
+					"name": "Create project template with only form key",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7d0ae3ca-fe2d-40eb-b5c8-9b03955babec",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"projectTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -4970,27 +4892,26 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 2-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-10-28T00:00:00.000Z\",\r\n    \"status\": \"completed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\"\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 1 => 2)",
+					"name": "Create project template with wrong form key",
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5003,27 +4924,26 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 2,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"wrong-key\"\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 2 => 1)",
+					"name": "Create project template with wrong model version",
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5036,27 +4956,26 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"question\": \"question 1\",\r\n    \"info\": \"info 1\",\r\n    \"aliases\": [\"key-1\", \"key_1\"],\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\r\n    \t\"version\": 1123\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 1 => 3)",
+					"name": "List project templates",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5067,29 +4986,24 @@
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 3,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 3 => 1)",
+					"name": "Get project template",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5100,29 +5014,25 @@
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates",
+								"{{projectTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete milestone",
+					"name": "Upgrade a project template with from, priceConfig, planConfig",
 					"request": {
-						"method": "DELETE",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5135,45 +5045,26 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 2\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"qa\",\t\r\n    \t \"version\": 2\t\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
-								"{{timelineId}}",
-								"milestones",
-								"{{milestoneId}}"
+								"projects",
+								"metadata",
+								"projectTemplates",
+								"{{projectTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Milestone Template",
-			"item": [
+				},
 				{
-					"name": "Create milestone template",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "3dbf8b29-2498-4b05-93de-14d809ccc285",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"milestoneTemplateId\", pm.response.json().id);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Upgrade a project template with wrong model version",
 					"request": {
 						"method": "POST",
 						"header": [
@@ -5183,29 +5074,31 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestoneTemplate 1\",\r\n    \"description\": \"description 1\",\r\n    \"duration\": 11,\r\n    \"type\": \"type3\",\r\n    \"order\": 1,\r\n    \"activeText\": \"activeText 1\",\r\n    \"completedText\": \"completedText 1\",\r\n    \"blockedText\": \"blockedText 1\",\r\n    \"plannedText\": \"planned Text 1\",\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": {{productTemplateId}},\r\n\t\"metadata\": {}\r\n}"
+							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    },\r\n    \"priceConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    },\r\n    \"planConfig\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t \"version\": 1234\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
+								"projectTemplates",
+								"{{projectTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create milestone template with invalid referenceId",
+					"name": "Upgrade a project template without define form, priceConfig, planConfig",
 					"request": {
 						"method": "POST",
 						"header": [
@@ -5215,31 +5108,33 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"name\": \"milestoneTemplate 3\",\r\n    \"description\": \"description 3\",\r\n    \"duration\": 33,\r\n    \"type\": \"type3\",\r\n    \"order\": 1,\r\n    \"activeText\": \"activeText 1\",\r\n    \"completedText\": \"completedText 1\",\r\n    \"blockedText\": \"blockedText 1\",\r\n    \"plannedText\": \"planned Text 1\",\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1000,\r\n\t\"metadata\": {}\r\n}"
+							"raw": "{\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
+								"projectTemplates",
+								"{{projectTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create milestone template with invalid data",
+					"name": "Update project template",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5247,31 +5142,32 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
+								"projectTemplates",
+								"{{projectTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Clone milestone template",
+					"name": "Update project template with define form, priceConfig, planConfig",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5279,32 +5175,32 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n   \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n    \"form\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    },\r\n    \"priceConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    },\r\n    \"planConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"clone"
+								"projectTemplates",
+								"{{projectTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Clone milestone template with invalid referenceId",
+					"name": "Update project template with wrong form, priceConfig, planConfig",
 					"request": {
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5312,32 +5208,32 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2000\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"app\",\r\n\t\"form\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    },\r\n    \"priceConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    },\r\n    \"planConfig\": {\r\n        \"key\": \"dev\",\r\n        \"version\": 1123\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"clone"
+								"projectTemplates",
+								"{{projectTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Clone milestone template with invalid sourceReferenceId",
+					"name": "Delete project template",
 					"request": {
-						"method": "POST",
+						"method": "DELETE",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5345,32 +5241,53 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-admin-40051333}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1000,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"new category\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"raw": "{{api-url}}/projects/metadata/projectTemplates/{{projectTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"clone"
+								"projectTemplates",
+								"{{projectTemplateId}}"
 							]
 						}
 					},
 					"response": []
-				},
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Product Templates",
+			"item": [
 				{
-					"name": "List milestone templates",
+					"name": "Create product template",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "b5aaf185-6026-4b58-b9b8-56616109cd5a",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"productTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5378,27 +5295,46 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-copilot-40051332}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"{{productCategoryId}}\",\r\n    \"subCategory\": \"app\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"template\": {\r\n      \"template1\": {\r\n        \"name\": \"template 1\",\r\n        \"details\": {\r\n          \"anyDetails\": \"any details 1\"\r\n        },\r\n        \"others\": [\"others 11\", \"others 12\"]\r\n      },\r\n      \"template2\": {\r\n        \"name\": \"template 2\",\r\n        \"details\": {\r\n          \"anyDetails\": \"any details 2\"\r\n        },\r\n        \"others\": [\"others 21\", \"others 22\"]\r\n      }\r\n    }\r\n  }"
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"raw": "{{api-url}}/projects/metadata/productTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
+								"productTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List milestone templates (filter)",
+					"name": "Create product template with form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d5a2af2e-97d2-415c-a533-1d52dd4003c7",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"productTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5406,37 +5342,31 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-copilot-40051332}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"dev\",\r\n\t\t\"version\": 1\r\n\t}\r\n  }"
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
-							],
-							"query": [
-								{
-									"key": "reference",
-									"value": "productTemplate"
-								},
-								{
-									"key": "referenceId",
-									"value": "{{productTemplateId}}"
-								}
+								"productTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "List milestone templates (sort)",
+					"name": "Create product template with wrong form key",
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5444,41 +5374,31 @@
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token-copilot-40051332}}"
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"wrong-key\"\r\n\t}\r\n  }"
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}&sort=order desc",
+							"raw": "{{api-url}}/projects/metadata/productTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates"
-							],
-							"query": [
-								{
-									"key": "reference",
-									"value": "productTemplate"
-								},
-								{
-									"key": "referenceId",
-									"value": "{{productTemplateId}}"
-								},
-								{
-									"key": "sort",
-									"value": "order desc"
-								}
+								"productTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get milestone template",
+					"name": "Create product template with wrong model version",
 					"request": {
-						"method": "GET",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5489,25 +5409,28 @@
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"name 1\",\r\n    \"productKey\": \"productKey 1\",\r\n    \"category\": \"key1\",\r\n    \"icon\": \"http://example.com/icon1.ico\",\r\n    \"brief\": \"brief 1\",\r\n    \"details\": \"details 1\",\r\n    \"aliases\": [\"product key 1\", \"product_key_1\"],\r\n    \"form\": {\r\n\t\t\"key\": \"dev\",\r\n\t\t\"version\": 1123\r\n\t}\r\n  }"
+						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone",
+					"name": "List product templates",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5518,29 +5441,24 @@
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 1 => 2)",
+					"name": "Get product template",
 					"request": {
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5551,27 +5469,23 @@
 								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 2,\r\n  \"reference\": \"productTemplate\",\r\n  \"referenceId\": {{productTemplateId}}\r\n}"
-						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 2 => 1)",
+					"name": "Update product template",
 					"request": {
 						"method": "PATCH",
 						"header": [
@@ -5586,27 +5500,27 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"productKey\":\"new productKey\",\r\n    \"category\":\"key1\",\r\n    \"icon\":\"http://example.com/icon-new.ico\",\r\n    \"brief\": \"new brief\",\r\n    \"details\": \"new details\",\r\n    \"aliases\":{\r\n      \"alias1\":\"scope 1\",\r\n      \"alias2\": [\"a\"]\r\n    },\r\n    \"template\":{\r\n      \"template1\":\"template 1\",\r\n      \"template2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 1 => 3)",
+					"name": "Delete product template",
 					"request": {
-						"method": "PATCH",
+						"method": "DELETE",
 						"header": [
 							{
 								"key": "Content-Type",
@@ -5619,305 +5533,324 @@
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 3,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone (order 3 => 1)",
+					"name": "Upgrade a product template with form",
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"value": "application/json"
+								"value": "application/json",
+								"type": "text"
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token}}",
+								"type": "text"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t\"version\": 2\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update milestone with metadata",
+					"name": "Upgrade a product template with wrong model version",
 					"request": {
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"value": "application/json"
+								"value": "application/json",
+								"type": "text"
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token}}",
+								"type": "text"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n\t\"name\": \"milestoneTemplate 5-updated\",\r\n\t\"description\": \"description 5-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type5-updated\",\r\n\t\"order\": 5,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1,\r\n\t\"metadata\": {\r\n        \"metadata1\": {\r\n          \"name\": \"metadata 1 - update\",\r\n          \"details\": {\r\n            \"anyDetails\": \"any details 1 - update\",\r\n            \"newDetails\": \"new\"\r\n          },\r\n          \"others\": [\"others new\"]\r\n        },\r\n        \"metadata3\": {\r\n          \"name\": \"metadata 3\",\r\n          \"details\": {\r\n            \"anyDetails\": \"any details 3\"\r\n          },\r\n          \"others\": [\"others 31\", \"others 32\"]\r\n        }\r\n      }\r\n}"
+							"raw": "{\r\n    \"form\": {\r\n    \t\"key\": \"dev\",\t\r\n    \t\"version\": 1234\r\n    }\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete milestone",
+					"name": "Upgrade a product template without define form",
 					"request": {
-						"method": "DELETE",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"value": "application/json"
+								"value": "application/json",
+								"type": "text"
 							},
 							{
 								"key": "Authorization",
-								"value": "Bearer {{jwt-token}}"
+								"value": "Bearer {{jwt-token}}",
+								"type": "text"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\r\n  \r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"raw": "{{api-url}}/projects/metadata/productTemplates/{{productTemplateId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"timelines",
+								"projects",
 								"metadata",
-								"milestoneTemplates",
-								"{{milestoneTemplateId}}"
+								"productTemplates",
+								"{{productTemplateId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Metadata",
+			"name": "Project Type",
 			"item": [
 				{
-					"name": "Get all metadata",
-					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "GET",
+					"name": "Create project type",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "fbc45946-a3f2-433a-8ec5-0af82b69d2bd",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"projectTypeId\", pm.response.json().key);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
 						"header": [
 							{
-								"key": "",
-								"value": "",
-								"type": "text"
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n        \"key\": \"new key\",\r\n        \"displayName\": \"new displayName\",\r\n        \"icon\": \"http://example.com/icon4.ico\",\r\n    \t\"question\": \"question 4\",\r\n    \t\"info\": \"info 4\",\r\n    \t\"aliases\": [\"key-41\", \"key_42\"],\r\n    \t\"metadata\": {}\r\n  }"
+						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata",
+							"raw": "{{api-url}}/projects/metadata/projectTypes",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata"
+								"metadata",
+								"projectTypes"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get all metadata with includeAllVersion",
+					"name": "List project types",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "GET",
-						"header": [],
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata?includeAllReferred=true",
+							"raw": "{{api-url}}/projects/metadata/projectTypes",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata"
-							],
-							"query": [
-								{
-									"key": "includeAllReferred",
-									"value": "true"
-								}
+								"metadata",
+								"projectTypes"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Form Version",
-			"item": [
+				},
 				{
-					"name": "List forms",
+					"name": "Get project type",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "GET",
-						"header": [],
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions",
+							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions"
+								"projectTypes",
+								"{{projectTypeId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular version",
+					"name": "Update project type",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"displayName\": \"Chatbot-updated\"\r\n  }"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}"
+								"projectTypes",
+								"{{projectTypeId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get latest version",
+					"name": "Delete project type",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}",
+							"raw": "{{api-url}}/projects/metadata/projectTypes/{{projectTypeId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}"
+								"projectTypes",
+								"{{projectTypeId}}"
 							]
 						}
 					},
 					"response": []
-				},
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Product Category",
+			"item": [
 				{
-					"name": "Create form",
+					"name": "Create product category",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "94f6be66-34cc-40c8-80c2-b27dd93ed527",
+								"id": "06156797-ceb2-4f8c-9448-5c453adb7b7a",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"formKey\", pm.response.json().key);",
-									"    pm.environment.set(\"formVersion\", pm.response.json().version);",
+									"    pm.environment.set(\"productCategoryId\", pm.response.json().key);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -5925,461 +5858,387 @@
 						}
 					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n        \"key\": \"generic\",\r\n        \"displayName\": \"new displayName\",\r\n        \"icon\": \"icon\",\r\n        \"question\": \"question\",\r\n        \"info\": \"info\",\r\n        \"aliases\": [\"key-1\", \"key-2\"]\r\n  }"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/dev/versions",
+							"raw": "{{api-url}}/projects/metadata/productCategories",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"dev",
-								"versions"
+								"productCategories"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update form",
+					"name": "List product categories",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"raw": "{{api-url}}/projects/metadata/productCategories",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}"
+								"productCategories"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete form",
+					"name": "Get product category",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "DELETE",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": ""
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}"
+								"productCategories",
+								"{{productCategoryId}}"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Form Revision",
-			"item": [
+				},
 				{
-					"name": "List all revision for version",
+					"name": "Update product category",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"displayName\": \"Chatbot-updated\"\r\n  }"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions",
+							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}",
-								"revisions"
+								"productCategories",
+								"{{productCategoryId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular revision",
+					"name": "Delete product category",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"key\":\"new key\",\r\n    \"category\":\"new category\",\r\n    \"scope\":{\r\n      \"scope1\":\"scope 1\",\r\n      \"scope2\": [\"a\"]\r\n    },\r\n    \"phases\":{\r\n      \"phase1\":\"phase 1\",\r\n      \"phase2\": {\r\n    \t\"another\": \"another\"\r\n      }\r\n    }\r\n  }"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}",
+							"raw": "{{api-url}}/projects/metadata/productCategories/{{productCategoryId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
 								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}",
-								"revisions",
-								"{{formRevision}}"
+								"productCategories",
+								"{{productCategoryId}}"
 							]
 						}
 					},
 					"response": []
+				}
+			],
+			"auth": {
+				"type": "bearer",
+				"bearer": [
+					{
+						"key": "token",
+						"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJhZG1pbmlzdHJhdG9yIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJwc2hhaDEiLCJleHAiOjI0NjI0OTQ2MTgsInVzZXJJZCI6IjQwMTM1OTc4IiwiaWF0IjoxNDYyNDk0MDE4LCJlbWFpbCI6InBzaGFoMUB0ZXN0LmNvbSIsImp0aSI6ImY0ZTFhNTE0LTg5ODAtNDY0MC04ZWM1LWUzNmUzMWE3ZTg0OSJ9.XuNN7tpMOXvBG1QwWRQROj7NfuUbqhkjwn39Vy4tR5I",
+						"type": "string"
+					}
+				]
+			},
+			"event": [
+				{
+					"listen": "prerequest",
+					"script": {
+						"id": "f0092ef5-e624-4c25-87b2-b6a9e4c81ec8",
+						"type": "text/javascript",
+						"exec": [
+							""
+						]
+					}
 				},
 				{
-					"name": "Create form",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "dbe5ec9f-022c-4ec5-b58c-d19c15430b61",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"formRevision\", pm.response.json().revision);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"listen": "test",
+					"script": {
+						"id": "9183c429-a5e0-4bf9-96a2-89f4d66e9b0d",
+						"type": "text/javascript",
+						"exec": [
+							""
+						]
+					}
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Project upgrade",
+			"item": [
+				{
+					"name": "Migrate project",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							},
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}",
-								"revisions"
+								"{{projectId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create for no exist key",
+					"name": "Migrate project (completed)",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							},
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3\n\t}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/no-exist-2222key36/versions/1/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"form",
-								"no-exist-2222key36",
-								"versions",
-								"1",
-								"revisions"
+								"{{projectId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete revision",
+					"name": "Migrate project with phase name",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "DELETE",
+						"method": "POST",
 						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							},
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"form",
-								"{{formKey}}",
-								"versions",
-								"{{formVersion}}",
-								"revisions",
-								"{{formRevision}}"
-							]
-						}
-					},
-					"response": []
-				}
-			]
-		},
-		{
-			"name": "Price Config Version",
-			"item": [
-				{
-					"name": "List price configs",
-					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "GET",
-						"header": [],
-						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions",
-							"host": [
-								"{{api-url}}"
-							],
-							"path": [
-								"projects",
-								"metadata",
-								"priceConfig",
-								"dev",
-								"versions"
+								"{{projectId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular version",
+					"name": "Migrate project with phase name (completed)",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "POST",
+						"header": [
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							},
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\n\t\t\"targetVersion\": \"v3\",\n\t\t\"defaultProductTemplateId\": 3,\n\t\t\"phaseName\": \"Custom phase name\"\n\t}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/upgrade",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}"
+								"{{projectId}}",
+								"upgrade"
 							]
 						}
 					},
 					"response": []
-				},
+				}
+			],
+			"description": "Request to migrate projects.",
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Timeline",
+			"item": [
 				{
-					"name": "Get latest version",
+					"name": "Create timeline",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "c066e7d4-537f-406e-a768-ec4bf73a2634",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"timelineId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n\t\"name\":\"new name\",\r\n\t\"description\":\"new description\",\r\n\t\"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n\t\"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n\t\"reference\": \"project\",\r\n\t\"referenceId\": {{projectId}}\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}",
+							"raw": "{{api-url}}/timelines",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}"
+								"timelines"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create priceConfig",
+					"name": "Create timeline with templateId",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "e440c87c-49ff-4443-b9bf-b44d4e9a480f",
+								"id": "ee729ed9-0072-4821-9141-3615ff66f728",
 								"exec": [
 									"pm.test(\"Status code is 201\", function () {",
 									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"priceKey\", pm.response.json().key);",
-									"    pm.environment.set(\"priceVersion\", pm.response.json().version);",
+									"    pm.environment.set(\"timelineId\", pm.response.json().id);",
 									"});"
 								],
 								"type": "text/javascript"
@@ -6387,810 +6246,3731 @@
 						}
 					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n    \"name\":\"new name\",\r\n    \"description\":\"new description\",\r\n    \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1,\r\n    \"templateId\": 1\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions",
+							"raw": "{{api-url}}/timelines",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"priceConfig",
-								"dev",
-								"versions"
+								"timelines"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update priceConfig",
+					"name": "Create timeline with invalid data",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "PATCH",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-connectAdmin-40051336}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n    \"startDate\":\"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-28T00:00:00.000Z\",\r\n    \"reference\": \"invalid\",\r\n    \"referenceId\": 0\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"raw": "{{api-url}}/timelines",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}"
+								"timelines"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete priceConfig",
+					"name": "List timelines (filter by reference and referenceId)",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "DELETE",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}",
+								"disabled": true
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}",
+								"type": "text"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": ""
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"raw": "{{api-url}}/timelines?reference=project&referenceId={{projectId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
-								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}"
+								"timelines"
+							],
+							"query": [
+								{
+									"key": "reference",
+									"value": "project"
+								},
+								{
+									"key": "referenceId",
+									"value": "{{projectId}}"
+								}
 							]
 						}
 					},
 					"response": []
-				}
-			],
-			"event": [
-				{
-					"listen": "prerequest",
-					"script": {
-						"id": "59182724-4332-4d76-90ea-f7520a7b1be9",
-						"type": "text/javascript",
-						"exec": [
-							""
-						]
-					}
 				},
 				{
-					"listen": "test",
-					"script": {
-						"id": "abc13dca-e8a4-4995-970f-00e5889a5f2d",
-						"type": "text/javascript",
-						"exec": [
+					"name": "Get timeline",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get timeline from DB",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update timeline",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-01T00:00:00.000Z\",\r\n    \"endDate\": null,\r\n    \"reference\": \"project\",\r\n    \"referenceId\": {{projectId}}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update timeline (startDate)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update timeline (endDate)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"timeline 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"startDate\": \"2018-05-04T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"reference\": \"project\",\r\n    \"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete timeline",
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Milestone",
+			"item": [
+				{
+					"name": "Create milestone",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "8fd1d5e9-8e6e-4cd7-9010-b855308be069",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"milestoneId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 3\",\r\n    \"description\": \"description 3\",\r\n    \"duration\": 4,\r\n    \"startDate\": \"2018-05-29T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-30T00:00:00.000Z\",\r\n    \"completionDate\": \"2018-05-31T00:00:00.000Z\",\r\n    \"status\": \"open\",\r\n    \"type\": \"type3\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            2,\r\n            3,\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 3\",\r\n    \"activeText\": \"activeText 3\",\r\n    \"completedText\": \"completedText 3\",\r\n    \"blockedText\": \"blockedText 3\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create milestone with invalid data",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-member-40051331}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"startDate\": \"2018-05-05T00:00:00.000Z\",\r\n    \"endDate\": \"2018-05-04T00:00:00.000Z\",\r\n    \"completionDate\": \"2018-05-04T00:00:00.000Z\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List milestones",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}",
+								"type": "text"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List milestones (sort)",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones?sort=order desc",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones"
+							],
+							"query": [
+								{
+									"key": "sort",
+									"value": "order desc"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get milestone",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-09-28T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone - paused",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"status\": \"paused\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"statusComment\": \"milestone paused\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone - resume",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"status\": \"resume\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"statusComment\": \"milestone resume\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (active)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 2-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-10-28T00:00:00.000Z\",\r\n    \"status\": \"active\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (completed)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 2-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-10-28T00:00:00.000Z\",\r\n    \"status\": \"completed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 1 => 2)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 2,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 2 => 1)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 1 => 3)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 3,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 3 => 1)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestone 1-updated\",\r\n    \"description\": \"description-updated\",\r\n    \"duration\": 3,\r\n    \"completionDate\": \"2018-05-07T00:00:00.000Z\",\r\n    \"status\": \"closed\",\r\n    \"type\": \"type2\",\r\n    \"details\": {\r\n        \"detail1\": {\r\n            \"subDetail1C\": 3\r\n        },\r\n        \"detail2\": [\r\n            4\r\n        ]\r\n    },\r\n    \"order\": 1,\r\n    \"plannedText\": \"plannedText 1-updated\",\r\n    \"activeText\": \"activeText 1-updated\",\r\n    \"completedText\": \"completedText 1-updated\",\r\n    \"blockedText\": \"blockedText 1-updated\"\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete milestone",
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/{{timelineId}}/milestones/{{milestoneId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"{{timelineId}}",
+								"milestones",
+								"{{milestoneId}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Milestone Template",
+			"item": [
+				{
+					"name": "Create milestone template",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "3dbf8b29-2498-4b05-93de-14d809ccc285",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"milestoneTemplateId\", pm.response.json().id);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestoneTemplate 1\",\r\n    \"description\": \"description 1\",\r\n    \"duration\": 11,\r\n    \"type\": \"type3\",\r\n    \"order\": 1,\r\n    \"activeText\": \"activeText 1\",\r\n    \"completedText\": \"completedText 1\",\r\n    \"blockedText\": \"blockedText 1\",\r\n    \"plannedText\": \"planned Text 1\",\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": {{productTemplateId}},\r\n\t\"metadata\": {}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create milestone template with invalid referenceId",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"name\": \"milestoneTemplate 3\",\r\n    \"description\": \"description 3\",\r\n    \"duration\": 33,\r\n    \"type\": \"type3\",\r\n    \"order\": 1,\r\n    \"activeText\": \"activeText 1\",\r\n    \"completedText\": \"completedText 1\",\r\n    \"blockedText\": \"blockedText 1\",\r\n    \"plannedText\": \"planned Text 1\",\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1000,\r\n\t\"metadata\": {}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create milestone template with invalid data",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Clone milestone template",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n   \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"clone"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Clone milestone template with invalid referenceId",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2000\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"clone"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Clone milestone template with invalid sourceReferenceId",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"sourceReference\": \"productTemplate\",\r\n    \"sourceReferenceId\": 1000,\r\n    \"reference\": \"productTemplate\",\r\n    \"referenceId\": 2\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/clone",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"clone"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List milestone templates",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List milestone templates (filter)",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							],
+							"query": [
+								{
+									"key": "reference",
+									"value": "productTemplate"
+								},
+								{
+									"key": "referenceId",
+									"value": "{{productTemplateId}}"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List milestone templates (sort)",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates?reference=productTemplate&referenceId={{productTemplateId}}&sort=order desc",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates"
+							],
+							"query": [
+								{
+									"key": "reference",
+									"value": "productTemplate"
+								},
+								{
+									"key": "referenceId",
+									"value": "{{productTemplateId}}"
+								},
+								{
+									"key": "sort",
+									"value": "order desc"
+								}
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get milestone template",
+					"request": {
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 1 => 2)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 2,\r\n  \"reference\": \"productTemplate\",\r\n  \"referenceId\": {{productTemplateId}}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 2 => 1)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n\t\"name\": \"milestoneTemplate 1-updated\",\r\n\t\"description\": \"description 1-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type1-updated\",\r\n\t\"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 1 => 3)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 3,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone (order 3 => 1)",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n  \"name\": \"milestoneTemplate 1-updated\",\r\n  \"description\": \"description 1-updated\",\r\n  \"duration\": 34,\r\n  \"type\": \"type1-updated\",\r\n  \"order\": 1,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update milestone with metadata",
+					"request": {
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n\t\"name\": \"milestoneTemplate 5-updated\",\r\n\t\"description\": \"description 5-updated\",\r\n\t\"duration\": 34,\r\n\t\"type\": \"type5-updated\",\r\n\t\"order\": 5,\r\n\t\"reference\": \"productTemplate\",\r\n\t\"referenceId\": 1,\r\n\t\"metadata\": {\r\n        \"metadata1\": {\r\n          \"name\": \"metadata 1 - update\",\r\n          \"details\": {\r\n            \"anyDetails\": \"any details 1 - update\",\r\n            \"newDetails\": \"new\"\r\n          },\r\n          \"others\": [\"others new\"]\r\n        },\r\n        \"metadata3\": {\r\n          \"name\": \"metadata 3\",\r\n          \"details\": {\r\n            \"anyDetails\": \"any details 3\"\r\n          },\r\n          \"others\": [\"others 31\", \"others 32\"]\r\n        }\r\n      }\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete milestone",
+					"request": {
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/timelines/metadata/milestoneTemplates/{{milestoneTemplateId}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"timelines",
+								"metadata",
+								"milestoneTemplates",
+								"{{milestoneTemplateId}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Metadata",
+			"item": [
+				{
+					"name": "Get all metadata",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [
+							{
+								"key": "",
+								"value": "",
+								"type": "text"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get all metadata with includeAllVersion",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata?includeAllReferred=true",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata"
+							],
+							"query": [
+								{
+									"key": "includeAllReferred",
+									"value": "true"
+								}
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Form Version",
+			"item": [
+				{
+					"name": "List forms",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get latest version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "94f6be66-34cc-40c8-80c2-b27dd93ed527",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"formKey\", pm.response.json().key);",
+									"    pm.environment.set(\"formVersion\", pm.response.json().version);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/dev/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"dev",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update form",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete form",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Form Revision",
+			"item": [
+				{
+					"name": "List all revision for version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}",
+								"revisions",
+								"{{formRevision}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create form",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "dbe5ec9f-022c-4ec5-b58c-d19c15430b61",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"formRevision\", pm.response.json().revision);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create for no exist key",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/no-exist-2222key36/versions/1/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"no-exist-2222key36",
+								"versions",
+								"1",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/form/{{formKey}}/versions/{{formVersion}}/revisions/{{formRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"form",
+								"{{formKey}}",
+								"versions",
+								"{{formVersion}}",
+								"revisions",
+								"{{formRevision}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Price Config Version",
+			"item": [
+				{
+					"name": "List price configs",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"dev",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get latest version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create priceConfig",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "e440c87c-49ff-4443-b9bf-b44d4e9a480f",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"priceKey\", pm.response.json().key);",
+									"    pm.environment.set(\"priceVersion\", pm.response.json().version);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"dev",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update priceConfig",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete priceConfig",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"event": [
+				{
+					"listen": "prerequest",
+					"script": {
+						"id": "59182724-4332-4d76-90ea-f7520a7b1be9",
+						"type": "text/javascript",
+						"exec": [
+							""
+						]
+					}
+				},
+				{
+					"listen": "test",
+					"script": {
+						"id": "abc13dca-e8a4-4995-970f-00e5889a5f2d",
+						"type": "text/javascript",
+						"exec": [
 							""
 						]
 					}
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Price Config Revision",
+			"item": [
+				{
+					"name": "List all revision for version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions/3/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"dev",
+								"versions",
+								"3",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}",
+								"revisions",
+								"{{priceRevision}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create price config",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "d53ed608-b21c-4d6f-bb68-c2beda1d631d",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"priceRevision\", pm.response.json().revision);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create for no exist key",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/no-exist-key/versions/1/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"no-exist-key",
+								"versions",
+								"1",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"priceConfig",
+								"{{priceKey}}",
+								"versions",
+								"{{priceVersion}}",
+								"revisions",
+								"{{priceRevision}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Plan Config Version",
+			"item": [
+				{
+					"name": "List plan configs",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/dev/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"dev",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get latest version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create plan config",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "97bc350a-0c4f-46a6-a315-a62b203b3ad2",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"planKey\", pm.response.json().key);",
+									"    pm.environment.set(\"planVersion\", pm.response.json().version);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/dev/versions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"dev",
+								"versions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Update plan config",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete plan config",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Plan Config Revision",
+			"item": [
+				{
+					"name": "List all revision for version",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Get a particular revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "GET",
+						"header": [],
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}",
+								"revisions",
+								"{{planRevision}}"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create plan config",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "a5373f1f-4beb-46f9-8538-10c938c204ba",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"planRevision\", pm.response.json().revision);",
+									"});"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"value": "application/json",
+								"type": "text"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create for no exist key",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/no-exist-key/versions/1/revisions",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"no-exist-key",
+								"versions",
+								"1",
+								"revisions"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Delete revision",
+					"request": {
+						"auth": {
+							"type": "bearer",
+							"bearer": [
+								{
+									"key": "token",
+									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
+									"type": "string"
+								}
+							]
+						},
+						"method": "DELETE",
+						"header": [
+							{
+								"key": "Content-Type",
+								"name": "Content-Type",
+								"type": "text",
+								"value": "application/json"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": ""
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"metadata",
+								"planConfig",
+								"{{planKey}}",
+								"versions",
+								"{{planVersion}}",
+								"revisions",
+								"{{planRevision}}"
+							]
+						}
+					},
+					"response": []
+				}
+			],
+			"protocolProfileBehavior": {}
+		},
+		{
+			"name": "Project Reports",
+			"item": [
+				{
+					"name": "summary",
+					"item": [
+						{
+							"name": "get report by admin",
+							"request": {
+								"method": "GET",
+								"header": [
+									{
+										"key": "Authorization",
+										"value": "Bearer {{jwt-token-admin-40051333}}",
+										"type": "text"
+									}
+								],
+								"url": {
+									"raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary",
+									"host": [
+										"{{api-url}}"
+									],
+									"path": [
+										"projects",
+										"{{projectId}}",
+										"reports"
+									],
+									"query": [
+										{
+											"key": "reportName",
+											"value": "summary"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get report by member",
+							"request": {
+								"method": "GET",
+								"header": [
+									{
+										"key": "Authorization",
+										"value": "Bearer {{jwt-token-member2-40051335}}",
+										"type": "text"
+									}
+								],
+								"url": {
+									"raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary",
+									"host": [
+										"{{api-url}}"
+									],
+									"path": [
+										"projects",
+										"{{projectId}}",
+										"reports"
+									],
+									"query": [
+										{
+											"key": "reportName",
+											"value": "summary"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get report with invalid project id",
+							"request": {
+								"method": "GET",
+								"header": [
+									{
+										"key": "Authorization",
+										"type": "text",
+										"value": "Bearer {{jwt-token-admin-40051333}}"
+									}
+								],
+								"url": {
+									"raw": "{{api-url}}/projects/123456/reports?reportName=summary",
+									"host": [
+										"{{api-url}}"
+									],
+									"path": [
+										"projects",
+										"123456",
+										"reports"
+									],
+									"query": [
+										{
+											"key": "reportName",
+											"value": "summary"
+										}
+									]
+								}
+							},
+							"response": []
+						},
+						{
+							"name": "get report with invalid report name",
+							"request": {
+								"method": "GET",
+								"header": [
+									{
+										"key": "Authorization",
+										"type": "text",
+										"value": "Bearer {{jwt-token-admin-40051333}}"
+									}
+								],
+								"url": {
+									"raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=summary123",
+									"host": [
+										"{{api-url}}"
+									],
+									"path": [
+										"projects",
+										"{{projectId}}",
+										"reports"
+									],
+									"query": [
+										{
+											"key": "reportName",
+											"value": "summary123"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					],
+					"protocolProfileBehavior": {},
+					"_postman_isSubFolder": true
+				},
+				{
+					"name": "projectBudget",
+					"item": [
+						{
+							"name": "get report by admin",
+							"request": {
+								"method": "GET",
+								"header": [
+									{
+										"key": "Authorization",
+										"type": "text",
+										"value": "Bearer {{jwt-token-admin-40051333}}"
+									}
+								],
+								"url": {
+									"raw": "{{api-url}}/projects/{{projectId}}/reports?reportName=projectBudget",
+									"host": [
+										"{{api-url}}"
+									],
+									"path": [
+										"projects",
+										"{{projectId}}",
+										"reports"
+									],
+									"query": [
+										{
+											"key": "reportName",
+											"value": "projectBudget"
+										}
+									]
+								}
+							},
+							"response": []
+						}
+					],
+					"protocolProfileBehavior": {},
+					"_postman_isSubFolder": true
+				}
+			],
+			"protocolProfileBehavior": {}
 		},
 		{
-			"name": "Price Config Revision",
+			"name": "Project Setting",
 			"item": [
 				{
-					"name": "List all revision for version",
+					"name": "Create project setting - double",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4",
+								"exec": [
+									"pm.test(\"Status code is 201\", function () {",
+									"    pm.response.to.have.status(201);",
+									"    pm.environment.set(\"settingId\", pm.response.json().id);",
+									"})"
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"1000\",\r\n    \"valueType\": \"double\",\r\n    \"projectId\": 1,\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"{{projectId}}",
+								"settings"
 							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create project setting - percentage",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "bf3aa19f-517c-4103-9250-82d7847e7477",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_fee\",\r\n    \"value\": \"18.88\",\r\n    \"valueType\": \"percentage\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t    \"topcoderRoles\": [\"Connect Copilot\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/dev/versions/3/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"dev",
-								"versions",
-								"3",
-								"revisions"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular revision",
+					"name": "Create project setting - for project = 2",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"2222\",\r\n    \"valueType\": \"double\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/2/settings",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"2",
+								"settings"
+							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create project setting - another estimation type",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_reference_program\",\r\n    \"value\": \"17800\",\r\n    \"valueType\": \"double\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\", \"copilot\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
+						},
+						"url": {
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"{{projectId}}",
+								"settings"
 							]
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "Create project setting with non estimation type",
+					"request": {
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_non_estimation\",\r\n    \"value\": \"8765\",\r\n    \"valueType\": \"string\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t  \"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}",
-								"revisions",
-								"{{priceRevision}}"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create price config",
+					"name": "Create project setting - duplicate key",
 					"event": [
 						{
 							"listen": "test",
 							"script": {
-								"id": "d53ed608-b21c-4d6f-bb68-c2beda1d631d",
+								"id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4",
 								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"priceRevision\", pm.response.json().revision);",
-									"});"
+									""
 								],
 								"type": "text/javascript"
 							}
 						}
 					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"1000\",\r\n    \"valueType\": \"double\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}",
-								"revisions"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create for no exist key",
+					"name": "Create project setting with invalid valueType",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"1000\",\r\n    \"valueType\": \"int1\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/no-exist-key/versions/1/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"no-exist-key",
-								"versions",
-								"1",
-								"revisions"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete revision",
+					"name": "Create project setting with invalid percentage value",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "DELETE",
+						"method": "POST",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\r\n    \"key\": \"markup_community\",\r\n    \"value\": \"200\",\r\n    \"valueType\": \"percentage\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/priceConfig/{{priceKey}}/versions/{{priceVersion}}/revisions/{{priceRevision}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"priceConfig",
-								"{{priceKey}}",
-								"versions",
-								"{{priceVersion}}",
-								"revisions",
-								"{{priceRevision}}"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Plan Config Version",
-			"item": [
+				},
 				{
-					"name": "List plan configs",
+					"name": "Create project setting with missing readPermission",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"1000\",\r\n    \"valueType\": \"int\",\r\n    \"projectId\": 1,\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/dev/versions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"dev",
-								"versions"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular version",
+					"name": "Create project setting with empty body",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get latest version",
+					"name": "Create project setting - not permitted",
+					"event": [
+						{
+							"listen": "test",
+							"script": {
+								"id": "7350de08-5111-44f8-8a4c-3d0c48bcd8d4",
+								"exec": [
+									""
+								],
+								"type": "text/javascript"
+							}
+						}
+					],
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "POST",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-member-40051331}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"key\": \"markup_topcoder_service\",\r\n    \"value\": \"1000\",\r\n    \"valueType\": \"double\",\r\n\t\"writePermission\": {\r\n\t  \t\"allowRule\": {\r\n\t    \t\"projectRoles\": [\"account_manager\"],\r\n\t    \t\"topcoderRoles\": [\"administrator\", \"Connect Admin\"]\r\n\t    },\r\n\t  \t\"denyRule\": {\r\n\t    \t\"topcoderRoles\": [\"Connect Copilot Manager\"]\r\n\t    }\r\n\t  },\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"administrator\", \"Connect Admin\", \"Connect Account Manager\"]\r\n\t  },\r\n\t\"metadata\": {}\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create plan config",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "97bc350a-0c4f-46a6-a315-a62b203b3ad2",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"planKey\", pm.response.json().key);",
-									"    pm.environment.set(\"planVersion\", pm.response.json().version);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "List project setting",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
+						"method": "GET",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
+							}
+						],
+						"url": {
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
+							"host": [
+								"{{api-url}}"
+							],
+							"path": [
+								"projects",
+								"{{projectId}}",
+								"settings"
 							]
-						},
-						"method": "POST",
+						}
+					},
+					"response": []
+				},
+				{
+					"name": "List project setting - 403",
+					"request": {
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-copilot-40051332}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/dev/versions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"dev",
-								"versions"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Update plan config",
+					"name": "List project setting - manager",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "PATCH",
+						"method": "GET",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-manager-40051334}}"
 							}
 						],
-						"body": {
-							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test111\"\r\n    \t}\r\n   }"
-						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}"
+								"{{projectId}}",
+								"settings"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete plan config",
+					"name": "Update project setting - (failed) change key",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "DELETE",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": ""
+							"raw": "{\r\n    \"key\": \"markup_community\",\r\n\t\"readPermission\": {\r\n\t  \t\"projectRoles\": [\"manager\"],\r\n\t    \"topcoderRoles\": [\"Connect Manager\"]\r\n\t  }\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings/{{settingId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}"
+								"{{projectId}}",
+								"settings",
+								"{{settingId}}"
 							]
 						}
 					},
 					"response": []
-				}
-			]
-		},
-		{
-			"name": "Plan Config Revision",
-			"item": [
+				},
 				{
-					"name": "List all revision for version",
+					"name": "Update project setting - change double to percentage",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"value\": \"35.60\",\r\n    \"valueType\": \"percentage\"\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings/{{settingId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}",
-								"revisions"
+								"{{projectId}}",
+								"settings",
+								"{{settingId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Get a particular revision",
+					"name": "Update project setting - non-existent project",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
+						"method": "PATCH",
+						"header": [
+							{
+								"key": "Content-Type",
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
+							}
+						],
+						"body": {
+							"mode": "raw",
+							"raw": "{\r\n    \"value\": \"30\",\r\n    \"valueType\": \"percentage\"\r\n}"
 						},
-						"method": "GET",
-						"header": [],
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}",
+							"raw": "{{api-url}}/projects/9999/settings/{{settingId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}",
-								"revisions",
-								"{{planRevision}}"
+								"9999",
+								"settings",
+								"{{settingId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create plan config",
-					"event": [
-						{
-							"listen": "test",
-							"script": {
-								"id": "a5373f1f-4beb-46f9-8538-10c938c204ba",
-								"exec": [
-									"pm.test(\"Status code is 201\", function () {",
-									"    pm.response.to.have.status(201);",
-									"    pm.environment.set(\"planRevision\", pm.response.json().revision);",
-									"});"
-								],
-								"type": "text/javascript"
-							}
-						}
-					],
+					"name": "Update project setting - non-existent project setting",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"value": "application/json",
-								"type": "text"
+								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n    \"value\": \"30\",\r\n    \"valueType\": \"percentage\"\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings/9999",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}",
-								"revisions"
+								"{{projectId}}",
+								"settings",
+								"9999"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Create for no exist key",
+					"name": "Update project setting - change readPermission",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
-						"method": "POST",
+						"method": "PATCH",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token-admin-40051333}}"
 							}
 						],
 						"body": {
 							"mode": "raw",
-							"raw": "{\r\n    \t\"config\": {\r\n    \t\t\"hello\": \"test\"\r\n    \t}\r\n   }"
+							"raw": "{\r\n\t\"readPermission\": {\r\n\t\t\"projectRoles\": [\"manager\"],\r\n\t\t\"topcoderRoles\": [\"Connect Manager\"]\r\n\t}\r\n}"
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/no-exist-key/versions/1/revisions",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings/{{settingId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"no-exist-key",
-								"versions",
-								"1",
-								"revisions"
+								"{{projectId}}",
+								"settings",
+								"{{settingId}}"
 							]
 						}
 					},
 					"response": []
 				},
 				{
-					"name": "Delete revision",
+					"name": "Delete project setting",
 					"request": {
-						"auth": {
-							"type": "bearer",
-							"bearer": [
-								{
-									"key": "token",
-									"value": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIiwiYWRtaW5pc3RyYXRvciJdLCJpc3MiOiJodHRwczovL2FwaS50b3Bjb2Rlci1kZXYuY29tIiwiaGFuZGxlIjoidGVzdDEiLCJleHAiOjI1NjMwNzY2ODksInVzZXJJZCI6IjQwMDUxMzMzIiwiaWF0IjoxNDYzMDc2MDg5LCJlbWFpbCI6InRlc3RAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.wKWUe0-SaiFVN-VR_-GwgFlvWaDkSbc8H55ktb9LAVw",
-									"type": "string"
-								}
-							]
-						},
 						"method": "DELETE",
 						"header": [
 							{
 								"key": "Content-Type",
-								"name": "Content-Type",
-								"type": "text",
 								"value": "application/json"
+							},
+							{
+								"key": "Authorization",
+								"value": "Bearer {{jwt-token}}"
 							}
 						],
 						"body": {
@@ -7198,25 +9978,23 @@
 							"raw": ""
 						},
 						"url": {
-							"raw": "{{api-url}}/projects/metadata/planConfig/{{planKey}}/versions/{{planVersion}}/revisions/{{planRevision}}",
+							"raw": "{{api-url}}/projects/{{projectId}}/settings/{{settingId}}",
 							"host": [
 								"{{api-url}}"
 							],
 							"path": [
 								"projects",
-								"metadata",
-								"planConfig",
-								"{{planKey}}",
-								"versions",
-								"{{planVersion}}",
-								"revisions",
-								"{{planRevision}}"
+								"{{projectId}}",
+								"settings",
+								"{{settingId}}"
 							]
 						}
 					},
 					"response": []
 				}
-			]
+			],
+			"protocolProfileBehavior": {}
 		}
-	]
-}
+	],
+	"protocolProfileBehavior": {}
+}
\ No newline at end of file
diff --git a/docs/swagger.yaml b/docs/swagger.yaml
index b653e9d..4912bf8 100644
--- a/docs/swagger.yaml
+++ b/docs/swagger.yaml
@@ -804,6 +804,726 @@ paths:
           description: Internal Server Error
           schema:
             $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+    get:
+      tags:
+        - workstream
+      operationId: findWorkStreams
+      security:
+        - Bearer: []
+      description: >-
+        Retrieve all project workstreams.
+      responses:
+        '200':
+          description: A list of project work streams
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/WorkStream'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+    post:
+      tags:
+        - workstream
+      operationId: addWorkStream
+      security:
+        - Bearer: []
+      description: >-
+        Create a work stream.
+      parameters:
+        - in: body
+          name: body
+          required: true
+          schema:
+            type: object
+            allOf:
+              - $ref: '#/definitions/WorkStreamRequest'
+      responses:
+        '200':
+          description: Returns the newly created project work stream
+          schema:
+            $ref: '#/definitions/WorkStream'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams/{workStreamId}':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/workStreamIdParam'
+    get:
+      tags:
+        - workstream
+      description: >-
+        Retrieve work stream by id.
+      security:
+        - Bearer: []
+      responses:
+        '200':
+          description: a project work stream
+          schema:
+            $ref: '#/definitions/WorkStream'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/workStreamIdParam'
+      operationId: getWorkStream
+    patch:
+      tags:
+        - workstream
+      operationId: updateWorkStream
+      security:
+        - Bearer: []
+      description: >-
+        Update a project work stream.
+      responses:
+        '200':
+          description: Successfully updated project work stream.
+          schema:
+            $ref: '#/definitions/WorkStream'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - name: body
+          in: body
+          required: true
+          schema:
+            $ref: '#/definitions/WorkStreamRequest'
+    delete:
+      tags:
+        - workstream
+      description: >-
+        Remove an existing project work stream.
+      security:
+        - Bearer: []
+      responses:
+        '204':
+          description: Project work stream successfully removed
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams/{workStreamId}/works':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/workStreamIdParam'
+    get:
+      tags:
+        - work
+      operationId: findWorks
+      security:
+        - Bearer: []
+      description: >-
+        Retrieve all works for given project and workstream.
+      parameters:
+        - name: fields
+          required: false
+          type: string
+          in: query
+          description: |
+            Comma separated list of project phase fields to return.
+        - name: sort
+          required: false
+          description: >
+            sort project phases by startDate, endDate, status, order. Default is
+            startDate asc
+          in: query
+          type: string
+      responses:
+        '200':
+          description: A list of project works
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/ProjectPhase'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project or workstream is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+    post:
+      tags:
+        - work
+      operationId: addWork
+      security:
+        - Bearer: []
+      description: >-
+        Create a work
+      parameters:
+        - in: body
+          name: body
+          required: true
+          schema:
+            type: object
+            allOf:
+              - $ref: '#/definitions/ProjectPhaseRequest'
+      responses:
+        '200':
+          description: Returns the newly created project work
+          schema:
+            $ref: '#/definitions/ProjectPhase'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project or workstream is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/phaseIdParam'
+      - $ref: '#/parameters/workStreamIdParam'
+    get:
+      tags:
+        - work
+      description: >-
+        Retrieve work by id.
+      security:
+        - Bearer: []
+      responses:
+        '200':
+          description: a project work
+          schema:
+            $ref: '#/definitions/ProjectPhase'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+      operationId: getWork
+    patch:
+      tags:
+        - work
+      operationId: updateWork
+      security:
+        - Bearer: []
+      description: >-
+        Update work for given project and workstream.
+      responses:
+        '200':
+          description: Successfully updated project work.
+          schema:
+            $ref: '#/definitions/ProjectPhase'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+        - name: body
+          in: body
+          required: true
+          schema:
+            $ref: '#/definitions/ProjectPhaseRequest'
+    delete:
+      tags:
+        - work
+      description: >-
+        Remove an existing work by id for given project and workstream.
+      security:
+        - Bearer: []
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+      responses:
+        '204':
+          description: Work successfully removed
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project or workstream is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/phaseIdParam'
+      - $ref: '#/parameters/workStreamIdParam'
+    get:
+      tags:
+        - work item
+      operationId: findWorkItems
+      security:
+        - Bearer: []
+      description: >-
+        Retrieve all work items for given project, workstream and phase.
+      responses:
+        '200':
+          description: A list of work items
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/PhaseProduct'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project, workstream or phase is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+    post:
+      tags:
+        - work item
+      operationId: addWorkItem
+      security:
+        - Bearer: []
+      description: Create a work item for given project, workstream and phase.
+      parameters:
+        - in: body
+          name: body
+          required: true
+          schema:
+            $ref: '#/definitions/PhaseProductRequest'
+      responses:
+        '200':
+          description: Returns the newly created work item
+          schema:
+            $ref: '#/definitions/PhaseProduct'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project, workstream or phase is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/workstreams/{workStreamId}/works/{phaseId}/workitems/{productId}':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/phaseIdParam'
+      - $ref: '#/parameters/workStreamIdParam'
+      - $ref: '#/parameters/productIdParam'
+    get:
+      tags:
+        - work item
+      description: >-
+        Retrieve work item by id for given project, workstream and phase.
+      security:
+        - Bearer: []
+      responses:
+        '200':
+          description: a work item
+          schema:
+            $ref: '#/definitions/PhaseProduct'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+      operationId: getWorkItem
+    patch:
+      tags:
+        - work item
+      operationId: updateWorkItem
+      security:
+        - Bearer: []
+      description: >-
+        Update a work item for given project, workstream and phase.
+      responses:
+        '200':
+          description: Successfully updated work item.
+          schema:
+            $ref: '#/definitions/PhaseProduct'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+        - name: body
+          in: body
+          required: true
+          schema:
+            $ref: '#/definitions/PhaseProductRequest'
+    delete:
+      tags:
+        - work item
+      description: >-
+        Remove an existing work item by id for given project, workstream and phase.
+      security:
+        - Bearer: []
+      parameters:
+        - $ref: '#/parameters/phaseIdParam'
+      responses:
+        '204':
+          description: Work item successfully removed
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project, workstream or phase is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/settings':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+    get:
+      tags:
+        - project settings
+      operationId: findProjectSettings
+      security:
+        - Bearer: []
+      description: >-
+        Retrieve all project settings. Only users with readPermission can get the setting
+      responses:
+        '200':
+          description: A list of project phases
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/ProjectSetting'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+    post:
+      tags:
+        - project settings
+      operationId: addProjectSetting
+      security:
+        - Bearer: []
+      description: >-
+        Create a project setting and create project estimation items based on estimation type.
+      parameters:
+        - in: body
+          name: body
+          required: true
+          schema:
+            type: object
+            allOf:
+              - $ref: '#/definitions/ProjectSettingRequest'
+      responses:
+        '200':
+          description: Returns the newly created project phase
+          schema:
+            $ref: '#/definitions/ProjectPhase'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/settings/{settingId}':
+    parameters:
+      - $ref: '#/parameters/projectIdParam'
+      - $ref: '#/parameters/settingIdParam'
+    patch:
+      tags:
+        - project settings
+      operationId: updateProjectSetting
+      security:
+        - Bearer: []
+      description: >-
+        Update a project setting. All user with write permission can edit the setting.
+      responses:
+        '200':
+          description: Successfully updated project setting.
+          schema:
+            $ref: '#/definitions/ProjectSetting'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - name: body
+          in: body
+          required: true
+          schema:
+            $ref: '#/definitions/ProjectSettingRequest'
+    delete:
+      tags:
+        - project settings
+      description: >-
+        Remove an existing project setting. All users who are connect managers and admins
+        access this endpoint.
+      security:
+        - Bearer: []
+      parameters:
+        - $ref: '#/parameters/settingIdParam'
+      responses:
+        '204':
+          description: Project setting successfully removed
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If project is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/{projectId}/estimations/{estimationId}/items':
+    get:
+      tags:
+        - Project Estimation Items
+      security:
+        - Bearer: []
+      description: get project estimation items
+      parameters:
+        - $ref: '#/parameters/projectIdParam'
+        - $ref: '#/parameters/projectEstimationIdParam'
+      responses:
+        '200':
+          description: List of project estimation items
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/ProjectEstimationItem'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Model not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Invalid server state or unknown error
+          schema:
+            $ref: '#/definitions/ErrorModel'
   '/projects/{projectId}/phases/{phaseId}/products':
     parameters:
       - $ref: '#/parameters/projectIdParam'
@@ -1960,7 +2680,216 @@ paths:
         '400':
           description: Bad request
           schema:
-            $ref: '#/definitions/ErrorModel'
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If organization config is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  /projects/metadata/workManagementPermission:
+    get:
+      tags:
+        - workManagementPermission
+      operationId: findWorkManagementPermissions
+      security:
+        - Bearer: []
+      description: >-
+        Retrieve all work management permissions. Only admin or connect admin can access
+        this endpoint.
+      parameters:
+        - name: filter
+          required: true
+          type: string
+          in: query
+          description: |
+            Url encoded list of Supported filters
+             - projectTemplateId (required)
+      responses:
+        '200':
+          description: A list of work management permissions
+          schema:
+            type: array
+            items:
+              $ref: '#/definitions/WorkManagementPermission'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+    post:
+      tags:
+        - workManagementPermission
+      operationId: addWorkManagementPermission
+      security:
+        - Bearer: []
+      description: >-
+        Create a work management permission. Only admin or connect admin can access
+        this endpoint.
+      parameters:
+        - in: body
+          name: body
+          required: true
+          schema:
+            $ref: '#/definitions/WorkManagementPermissionCreateRequest'
+      responses:
+        '200':
+          description: Returns the newly created work management permission
+          schema:
+            $ref: '#/definitions/WorkManagementPermission'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+  '/projects/metadata/workManagementPermission/{id}':
+    get:
+      tags:
+        - workManagementPermission
+      description: Retrieve work management permission by id. Only admin or connect admin can access
+        this endpoint.
+      security:
+        - Bearer: []
+      responses:
+        '200':
+          description: a project type
+          schema:
+            $ref: '#/definitions/WorkManagementPermission'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/permissionIdParam'
+      operationId: getWorkManagementPermission
+    patch:
+      tags:
+        - workManagementPermission
+      operationId: updateWorkManagementPermission
+      security:
+        - Bearer: []
+      description: >-
+        Update a work management permission. Only admin or connect admin can access
+        this endpoint.
+      responses:
+        '200':
+          description: Successfully updated work management permission.
+          schema:
+            $ref: '#/definitions/WorkManagementPermission'
+        '400':
+          description: Bad request
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: Not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/permissionIdParam'
+        - name: body
+          in: body
+          required: true
+          schema:
+            $ref: '#/definitions/WorkManagementPermissionCreateRequest'
+    delete:
+      tags:
+        - workManagementPermission
+      description: >-
+        Remove an existing work management permission. Only admin or connect admin can
+        access this endpoint.
+      security:
+        - Bearer: []
+      parameters:
+        - $ref: '#/parameters/permissionIdParam'
+      responses:
+        '204':
+          description: Work management permission successfully removed
+        '401':
+          description: Unauthorized
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '403':
+          description: Forbidden
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '404':
+          description: If work management permission is not found
+          schema:
+            $ref: '#/definitions/ErrorModel'
+        '500':
+          description: Internal Server Error
+          schema:
+            $ref: '#/definitions/ErrorModel'
+
+  '/projects/{projectId}/permissions':
+    get:
+      tags:
+        - permissions
+      description: Retrieve permissions.
+      security:
+        - Bearer: []
+      responses:
+        '200':
+          description: permissions
+          schema:
+            title: Single work management permission response object
+            type: object
+            example:
+              'work.create': true
+              'workItem.edit': true
+                    
         '401':
           description: Unauthorized
           schema:
@@ -1970,13 +2899,16 @@ paths:
           schema:
             $ref: '#/definitions/ErrorModel'
         '404':
-          description: If organization config is not found
+          description: Not found
           schema:
             $ref: '#/definitions/ErrorModel'
         '500':
           description: Internal Server Error
           schema:
             $ref: '#/definitions/ErrorModel'
+      parameters:
+        - $ref: '#/parameters/projectIdParam'
+      operationId: getPermissions
   /timelines:
     get:
       tags:
@@ -3660,6 +4592,22 @@ parameters:
     type: integer
     format: int64
     minimum: 1
+  workStreamIdParam:
+    name: workStreamId
+    in: path
+    description: work stream identifier
+    required: true
+    type: integer
+    format: int64
+    minimum: 1
+  settingIdParam:
+    name: settingId
+    in: path
+    description: project setting identifier
+    required: true
+    type: integer
+    format: int64
+    minimum: 1
   productIdParam:
     name: productId
     in: path
@@ -3721,6 +4669,13 @@ parameters:
     required: true
     type: integer
     format: int64
+  permissionIdParam:
+    name: id
+    in: path
+    description: work management permission id
+    required: true
+    type: integer
+    format: int64
   pageParam:
     name: page
     in: query
@@ -3824,15 +4779,14 @@ parameters:
     description: manager filter
     required: false
     type: string
+  projectEstimationIdParam:
+    name: estimationId
+    in: path
+    description: project estimation identifier
+    required: true
+    type: integer
+    format: int64
 definitions:
-  ResponseMetadata:
-    title: Metadata object for a response
-    type: object
-    properties:
-      totalCount:
-        type: integer
-        format: int64
-        description: Total count of the objects
   ErrorModel:
     type: object
     properties:
@@ -3902,7 +4856,7 @@ definitions:
             - buildingBlockKey
           properties:
             conditions:
-                type: string
+              type: string
             price:
               type: number
               format: float
@@ -3915,7 +4869,7 @@ definitions:
             metadata:
               type: object
             buildingBlockKey:
-                type: string
+              type: string
       type:
         type: string
         description: project type
@@ -4476,6 +5430,12 @@ definitions:
       name:
         type: string
         description: the project phase name
+      description:
+        type: string
+        description: the project phase short description
+      requirements:
+        type: string
+        description: the project phase requirements
       status:
         type: string
         description: the project phase status
@@ -4954,12 +5914,16 @@ definitions:
       blockedText:
         type: string
         description: the milestone blocked text
+      statusComment:
+        type: string
+        description: the milestone status history comment
   Milestone:
     title: Milestone object
     allOf:
       - type: object
         required:
           - id
+          - statusHistory
           - createdAt
           - createdBy
           - updatedAt
@@ -4969,6 +5933,8 @@ definitions:
             type: number
             format: int64
             description: the id
+          statusHistory:
+            $ref: '#/definitions/StatusHistory'
           createdAt:
             type: string
             description: Datetime (GMT) when object was created
@@ -5117,6 +6083,10 @@ definitions:
         type: array
         items:
           $ref: '#/definitions/ProductCategory'
+      buildingBlocks:
+                type: array
+                items:
+                  $ref: '#/definitions/BuildingBlock'
   ProjectMemberInvite:
     type: object
     properties:
@@ -5352,3 +6322,320 @@ definitions:
       config:
         description: config json
         type: object
+  ProjectSetting:
+    title: Project setting object
+    allOf:
+      - type: object
+        required:
+          - id
+          - createdAt
+          - createdBy
+          - updatedAt
+          - updatedBy
+        properties:
+          id:
+            type: number
+            format: int64
+            description: the id
+          createdAt:
+            type: string
+            description: Datetime (GMT) when object was created
+            readOnly: true
+          createdBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User who created this object
+            readOnly: true
+          updatedAt:
+            type: string
+            description: READ-ONLY. Datetime (GMT) when object was updated
+            readOnly: true
+          updatedBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User that last updated this object
+            readOnly: true
+      - $ref: '#/definitions/ProjectSettingRequest'
+  ProjectSettingRequest:
+    title: Project setting request object
+    type: object
+    required:
+      - key
+      - value
+      - valueType
+      - readPermission
+      - writePermission
+      - metadata
+    properties:
+      key:
+        type: string
+        description: the project setting key
+      value:
+        type: string
+        description: the project setting value
+      valueType:
+        type: string
+        description: the project setting value type
+      readPermission:
+        type: object
+        description: the project setting read Permission
+      writePermission:
+        type: object
+        description: the project setting write Permission
+      metadata:
+        type: object
+        description: the project setting metadata
+  WorkStream:
+    title: Work stream object
+    allOf:
+      - type: object
+        required:
+          - id
+          - createdAt
+          - createdBy
+          - updatedAt
+          - updatedBy
+        properties:
+          id:
+            type: number
+            format: int64
+            description: the id
+          projectId:
+            type: number
+            format: int64
+            description: the project id
+          createdAt:
+            type: string
+            description: Datetime (GMT) when object was created
+            readOnly: true
+          createdBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User who created this object
+            readOnly: true
+          updatedAt:
+            type: string
+            description: READ-ONLY. Datetime (GMT) when object was updated
+            readOnly: true
+          updatedBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User that last updated this object
+            readOnly: true
+      - $ref: '#/definitions/WorkStreamRequest'
+  WorkStreamRequest:
+    title: Work stream request object
+    type: object
+    required:
+      - name
+      - status
+      - type
+    properties:
+      name:
+        type: string
+        description: the work stream name
+      status:
+        type: string
+        description: the work stream status
+      type:
+        type: string
+        description: the type
+  WorkManagementPermissionCreateRequest:
+    title: Work Management Permission request object
+    type: object
+    required:
+      - policy
+      - permission
+      - projectTemplateId
+    properties:
+      policy:
+        type: string
+        description: the policy
+      permission:
+        type: object
+        description: the permission
+      projectTemplateId:
+        type: number
+        format: int64
+        description: the template id
+  WorkManagementPermission:
+    title: Work Management Permission object
+    allOf:
+      - type: object
+        required:
+          - id
+          - policy
+          - permission
+          - projectTemplateId
+          - createdAt
+          - createdBy
+          - updatedAt
+          - updatedBy
+        properties:
+          id:
+            type: number
+            format: int64
+            description: the id
+          policy:
+            type: string
+            description: the policy
+          permission:
+            type: object
+            description: the permission
+          projectTemplateId:
+            type: number
+            format: int64
+            description: the template id
+          createdAt:
+            type: string
+            description: Datetime (GMT) when object was created
+            readOnly: true
+          createdBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User who created this object
+            readOnly: true
+          updatedAt:
+            type: string
+            description: READ-ONLY. Datetime (GMT) when object was updated
+            readOnly: true
+          updatedBy:
+            type: integer
+            format: int64
+            description: READ-ONLY. User that last updated this object
+            readOnly: true
+      - $ref: '#/definitions/WorkManagementPermissionCreateRequest'
+  StatusHistory:
+    title: Status history object
+    type: object
+    required:
+      - id
+      - status
+      - reference
+      - referenceId
+      - comment
+    properties:
+      id:
+        type: string
+        description: the id
+      status:
+        type: string
+        description: the status
+      reference:
+        type: string
+        description: the referenced model
+      referenceId:
+        type: string
+        description: the referenced id
+      comment:
+        type: string
+        description: the comment
+      createdAt:
+        type: string
+        description: Datetime (GMT) when object was created
+        readOnly: true
+      createdBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User who created this object
+        readOnly: true
+      updatedAt:
+        type: string
+        description: READ-ONLY. Datetime (GMT) when object was updated
+        readOnly: true
+      updatedBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User that last updated this object
+        readOnly: true
+  BuildingBlock:
+    title: BuildingBlock object
+    type: object
+    required:
+      - id
+      - key
+      - config
+    properties:
+      id:
+        type: integer
+        format: int64
+        description: the id
+      key:
+        type: string
+        description: building block key. Unique field.
+      config:
+        type: object
+        description: building block config
+      createdAt:
+        type: string
+        description: Datetime (GMT) when object was created
+        readOnly: true
+      createdBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User who created this object
+        readOnly: true
+      updatedAt:
+        type: string
+        description: READ-ONLY. Datetime (GMT) when object was updated
+        readOnly: true
+      updatedBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User that last updated this object
+        readOnly: true
+  ProjectEstimationItem:
+    title: ProjectEstimationItem object
+    type: object
+    required:
+      - id
+      - projectEstimationId
+      - price
+      - type
+      - markupUsedReference
+      - markupUsedReferenceId
+      - metadata
+    properties:
+      id:
+        type: integer
+        format: int64
+        description: the id
+      projectEstimationId:
+        type: integer
+        format: int64
+        description: the ProjectEstimation id
+      price:
+        type: number
+        format: float
+        description: the price of this estimation item
+      type:
+        type: string
+        description: the type of this estimation
+      markupUsedReference:
+        type: string
+        description: the reference type of this estimation. Can be "buildingBlock" for example
+      markupUsedReferenceId:
+        type: integer
+        format: int64
+        description: the reference object id
+      metadata:
+        type: object
+        description: the metadata of this item
+      createdAt:
+        type: string
+        description: Datetime (GMT) when object was created
+        readOnly: true
+      createdBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User who created this object
+        readOnly: true
+      updatedAt:
+        type: string
+        description: READ-ONLY. Datetime (GMT) when object was updated
+        readOnly: true
+      updatedBy:
+        type: integer
+        format: int64
+        description: READ-ONLY. User that last updated this object
+        readOnly: true
\ No newline at end of file
diff --git a/local/seed/seedMetadata.js b/local/seed/seedMetadata.js
index 6cf6cfa..6af36f2 100644
--- a/local/seed/seedMetadata.js
+++ b/local/seed/seedMetadata.js
@@ -37,7 +37,45 @@ module.exports = (targetUrl, token) => {
       'Authorization': 'Bearer ' + token
     }
 
-    let promises = _(data.result.content.projectTypes).map(pt=>{
+    let promises
+
+    promises = _(data.result.content.forms).orderBy(['key', 'asc'], ['version', 'asc']).map(pt=>{
+      const param = _.omit(pt, ['id', 'version', 'revision', 'key']);
+      return axios
+        .post(destUrl + `metadata/form/${pt.key}/versions`, param, {headers:headers})
+        .catch((err) => {
+          const errMessage = _.get(err, 'response.data.message', '');
+          console.log(`Failed to create form with key=${pt.key} version=${pt.version}.`, errMessage)
+        })
+    });
+
+    await Promise.all(promises);
+
+    promises = _(data.result.content.planConfigs).orderBy(['key', 'asc'], ['version', 'asc']).map(pt=>{
+      const param = _.omit(pt, ['id', 'version', 'revision', 'key']);
+      return axios
+        .post(destUrl + `metadata/planConfig/${pt.key}/versions`, param, {headers:headers})
+        .catch((err) => {
+          const errMessage = _.get(err, 'response.data.message', '');
+          console.log(`Failed to create planConfig with key=${pt.key} version=${pt.version}.`, errMessage)
+        })
+    });
+
+    await Promise.all(promises);
+
+    promises = _(data.result.content.priceConfigs).orderBy(['key', 'asc'], ['version', 'asc']).map(pt=>{
+      const param = _.omit(pt, ['id', 'version', 'revision', 'key']);
+      return axios
+        .post(destUrl + `metadata/priceConfig/${pt.key}/versions`, param, {headers:headers})
+        .catch((err) => {
+          const errMessage = _.get(err, 'response.data.message', '');
+          console.log(`Failed to create priceConfig with key=${pt.key} version=${pt.version}.`, errMessage)
+        })
+    });
+
+    await Promise.all(promises);
+
+    promises = _(data.result.content.projectTypes).map(pt=>{
       return axios
         .post(destUrl+'metadata/projectTypes', pt, {headers:headers})
         .catch((err) => {
diff --git a/local/seed/seedProjects.js b/local/seed/seedProjects.js
index 9467758..a84b82d 100644
--- a/local/seed/seedProjects.js
+++ b/local/seed/seedProjects.js
@@ -1,4 +1,5 @@
 import util from '../../src/tests/util';
+import models from '../../src/models';
 
 const axios = require('axios');
 const Promise = require('bluebird');
@@ -60,6 +61,21 @@ module.exports = (targetUrl, token) => {
           });
         }
 
+        await models.ProjectEstimation.create({
+          projectId,
+          buildingBlockKey: 'BLOCK_KEY',
+          conditions: '( HAS_DEV_DELIVERABLE && ONLY_ONE_OS_MOBILE && CA_NEEDED )',
+          price: 6500.50,
+          quantity: 10,
+          minTime: 35,
+          maxTime: 35,
+          metadata: {
+            deliverable: 'dev-qa',
+          },
+          createdBy: 1,
+          updatedBy: 1,
+        });
+
         // creating invitations
         if (Array.isArray(invites)) {
           let promises = []
diff --git a/migrations/20190502_status_history_create.sql b/migrations/20190502_status_history_create.sql
new file mode 100644
index 0000000..cac3875
--- /dev/null
+++ b/migrations/20190502_status_history_create.sql
@@ -0,0 +1,29 @@
+--
+-- Create table status history
+--
+
+CREATE TABLE status_history (
+    id bigint,
+    "reference" character varying(45) NOT NULL,
+    "referenceId" bigint NOT NULL,
+    "status" character varying(45) NOT NULL,
+    "comment" text,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "createdBy" integer NOT NULL,
+    "updatedBy" integer NOT NULL
+);
+
+CREATE SEQUENCE status_history_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE status_history_id_seq OWNED BY status_history.id;
+
+ALTER TABLE ONLY status_history ALTER COLUMN id SET DEFAULT nextval('status_history_id_seq'::regclass);
+
+ALTER TABLE ONLY status_history
+    ADD CONSTRAINT status_history_pkey PRIMARY KEY (id);
\ No newline at end of file
diff --git a/migrations/20190611_extract_scope_from_project_templates_2.sql b/migrations/20190611_extract_scope_from_project_templates_2.sql
new file mode 100644
index 0000000..0a0cbaa
--- /dev/null
+++ b/migrations/20190611_extract_scope_from_project_templates_2.sql
@@ -0,0 +1,12 @@
+--
+-- FIX for 20190316_extract_scope_from_project_templates.sql
+-- apply created auto-increments sequences to `id` columns
+
+ALTER TABLE form
+    ALTER COLUMN id SET DEFAULT nextval('form_id_seq');
+
+ALTER TABLE price_config
+    ALTER COLUMN id SET DEFAULT nextval('price_config_id_seq');
+
+ALTER TABLE plan_config
+    ALTER COLUMN id SET DEFAULT nextval('plan_config_id_seq');
diff --git a/migrations/20190620_migrate_product_templates.sql b/migrations/20190620_migrate_product_templates.sql
new file mode 100644
index 0000000..fdd6631
--- /dev/null
+++ b/migrations/20190620_migrate_product_templates.sql
@@ -0,0 +1,11 @@
+--
+-- UPDATE EXISTING TABLES:
+--   template:
+--     remove `sections` if exists and change `questions` to `sections`
+
+--
+-- product_templates
+
+UPDATE product_templates
+SET template = (template::jsonb #- '{questions}' #- '{sections}') || jsonb_build_object('sections', template::jsonb ->'questions')
+WHERE template::jsonb ? 'questions';
diff --git a/migrations/20190624_workStream.sql b/migrations/20190624_workStream.sql
new file mode 100644
index 0000000..fa975de
--- /dev/null
+++ b/migrations/20190624_workStream.sql
@@ -0,0 +1,82 @@
+--
+-- CREATE NEW TABLE:
+--   work_streams
+--
+CREATE TABLE work_streams (
+    id bigint NOT NULL,
+    "name" character varying(255) NOT NULL,
+    "type" character varying(45) NOT NULL,
+    "status" character varying(255) NOT NULL,
+    "projectId" bigint NOT NULL,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "deletedBy" bigint,
+    "createdBy" bigint NOT NULL,
+    "updatedBy" bigint NOT NULL
+);
+
+CREATE SEQUENCE work_streams_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE work_streams_id_seq OWNED BY work_streams.id;
+
+ALTER TABLE work_streams
+    ALTER COLUMN id SET DEFAULT nextval('work_streams_id_seq');
+
+ALTER TABLE ONLY work_streams
+    ADD CONSTRAINT "work_streams_pkey" PRIMARY KEY (id);
+
+ALTER TABLE ONLY work_streams
+    ADD CONSTRAINT "work_streams_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES projects(id) ON UPDATE CASCADE ON DELETE SET NULL;
+
+--
+-- CREATE NEW TABLE:
+--   work_management_permissions
+--
+CREATE TABLE work_management_permissions (
+    id bigint NOT NULL,
+    "policy" character varying(255) NOT NULL,
+    "permission" json NOT NULL,
+    "projectTemplateId" bigint NOT NULL,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "deletedBy" bigint,
+    "createdBy" bigint NOT NULL,
+    "updatedBy" bigint NOT NULL
+);
+
+CREATE SEQUENCE work_management_permissions_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE work_management_permissions_id_seq OWNED BY work_management_permissions.id;
+
+ALTER TABLE work_management_permissions
+    ALTER COLUMN id SET DEFAULT nextval('work_management_permissions_id_seq');
+
+--
+-- CREATE NEW TABLE:
+--   phase_work_streams
+--
+CREATE TABLE phase_work_streams (
+    "workStreamId" bigint NOT NULL,
+    "phaseId" bigint NOT NULL
+);
+
+ALTER TABLE ONLY phase_work_streams
+    ADD CONSTRAINT "phase_work_streams_pkey" PRIMARY KEY ("workStreamId", "phaseId");
+
+ALTER TABLE ONLY phase_work_streams
+    ADD CONSTRAINT "phase_work_streams_phaseId_fkey" FOREIGN KEY ("phaseId") REFERENCES project_phases(id) ON UPDATE CASCADE ON DELETE CASCADE;
+
+ALTER TABLE ONLY phase_work_streams
+    ADD CONSTRAINT "phase_work_streams_workStreamId_fkey" FOREIGN KEY ("workStreamId") REFERENCES work_streams(id) ON UPDATE CASCADE ON DELETE CASCADE;
diff --git a/migrations/20190719_project_settings_and_project_estimation_items.sql b/migrations/20190719_project_settings_and_project_estimation_items.sql
new file mode 100644
index 0000000..cf6086b
--- /dev/null
+++ b/migrations/20190719_project_settings_and_project_estimation_items.sql
@@ -0,0 +1,74 @@
+-- CREATE NEW TABLES:
+--   project_settings
+--   project_estimation_items
+--
+
+--
+-- project_settings
+--
+
+CREATE TABLE project_settings (
+    id bigint NOT NULL,
+    key character varying(255),
+    value character varying(255),
+    "valueType" character varying(255),
+    "projectId" bigint NOT NULL,
+    metadata json NOT NULL DEFAULT '{}'::json,
+    "readPermission" json NOT NULL DEFAULT '{}'::json,
+    "writePermission" json NOT NULL DEFAULT '{}'::json,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "deletedBy" bigint,
+    "createdBy" bigint NOT NULL,
+    "updatedBy" bigint NOT NULL,
+    CONSTRAINT project_settings_pkey PRIMARY KEY (id)
+);
+
+CREATE SEQUENCE project_settings_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE project_settings_id_seq OWNED BY project_settings.id;
+
+ALTER TABLE project_settings
+    ALTER COLUMN id SET DEFAULT nextval('project_settings_id_seq');
+
+ALTER TABLE project_settings
+    ADD CONSTRAINT project_settings_key_project_id UNIQUE (key, "projectId");
+
+--
+-- project_estimation_items
+--
+
+CREATE TABLE project_estimation_items (
+    id bigint NOT NULL,
+    "projectEstimationId" bigint NOT NULL,
+    price double precision NOT NULL,
+    type character varying(255) NOT NULL,
+    "markupUsedReference" character varying(255) NOT NULL,
+    "markupUsedReferenceId" bigint NOT NULL,
+    metadata json NOT NULL DEFAULT '{}'::json,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "deletedBy" bigint,
+    "createdBy" bigint NOT NULL,
+    "updatedBy" bigint NOT NULL,
+    CONSTRAINT project_estimation_items_pkey PRIMARY KEY (id)
+);
+
+CREATE SEQUENCE project_estimation_items_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE project_estimation_items_id_seq OWNED BY form.id;
+
+ALTER TABLE project_estimation_items
+    ALTER COLUMN id SET DEFAULT nextval('project_estimation_items_id_seq');
diff --git a/migrations/20190720_project_building_block.sql b/migrations/20190720_project_building_block.sql
new file mode 100644
index 0000000..2342ea0
--- /dev/null
+++ b/migrations/20190720_project_building_block.sql
@@ -0,0 +1,35 @@
+--
+-- CREATE NEW TABLE:
+--   building_blocks
+--
+CREATE TABLE building_blocks (
+    id bigint NOT NULL,
+    "key" character varying(255) NOT NULL,
+    "config" json NOT NULL DEFAULT '{}'::json,
+    "privateConfig" json NOT NULL DEFAULT '{}'::json,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "deletedBy" bigint,
+    "createdBy" bigint NOT NULL,
+    "updatedBy" bigint NOT NULL
+);
+
+ALTER TABLE building_blocks
+    ADD CONSTRAINT building_blocks_key_uniq UNIQUE (key);
+
+CREATE SEQUENCE building_blocks_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+ALTER SEQUENCE building_blocks_id_seq OWNED BY building_blocks.id;
+
+ALTER TABLE building_blocks
+    ALTER COLUMN id SET DEFAULT nextval('building_blocks_id_seq');
+
+ALTER TABLE ONLY building_blocks
+    ADD CONSTRAINT building_blocks_pkey PRIMARY KEY (id);
+
diff --git a/migrations/20190729_project_phase_description_and_requirements.sql b/migrations/20190729_project_phase_description_and_requirements.sql
new file mode 100644
index 0000000..9ad9e35
--- /dev/null
+++ b/migrations/20190729_project_phase_description_and_requirements.sql
@@ -0,0 +1,8 @@
+--
+-- UPDATE EXISTING TABLES:
+--   project_phases
+--     description column: added
+--     requirements column: added
+
+ALTER TABLE project_phases ADD COLUMN "description" character varying(255);
+ALTER TABLE project_phases ADD COLUMN "requirements" text;
\ No newline at end of file
diff --git a/migrations/20190729_scope_change_requests.sql b/migrations/20190729_scope_change_requests.sql
new file mode 100644
index 0000000..fe382c3
--- /dev/null
+++ b/migrations/20190729_scope_change_requests.sql
@@ -0,0 +1,42 @@
+--
+-- CREATE NEW TABLE:
+--   scope_change_requests
+--
+
+CREATE TABLE scope_change_requests
+(
+    id bigint NOT NULL,
+    "projectId" bigint NOT NULL,
+    "oldScope" json NOT NULL,
+    "newScope" json NOT NULL,
+    status character varying(45) NOT NULL,
+    "deletedAt" timestamp with time zone,
+    "createdAt" timestamp with time zone,
+    "updatedAt" timestamp with time zone,
+    "approvedAt" timestamp with time zone,
+    "deletedBy" integer,
+    "createdBy" integer NOT NULL,
+    "updatedBy" integer NOT NULL,
+    "approvedBy" integer
+);
+
+
+CREATE SEQUENCE scope_change_requests_id_seq
+    START WITH 1
+    INCREMENT BY 1
+    NO MINVALUE
+    NO MAXVALUE
+    CACHE 1;
+
+
+ALTER SEQUENCE scope_change_requests_id_seq OWNED BY scope_change_requests.id;
+
+ALTER TABLE scope_change_requests
+    ALTER COLUMN id SET DEFAULT nextval('scope_change_requests_id_seq');
+
+ALTER TABLE ONLY scope_change_requests
+   ADD CONSTRAINT scope_change_requests_pkey PRIMARY KEY (id);
+
+ALTER TABLE ONLY scope_change_requests
+    ADD CONSTRAINT "scope_change_requests_projectId_fkey" FOREIGN KEY ("projectId")
+    REFERENCES projects(id) MATCH SIMPLE ON UPDATE CASCADE ON DELETE CASCADE;
diff --git a/migrations/20190927_milestone_texts_not_required.sql b/migrations/20190927_milestone_texts_not_required.sql
new file mode 100644
index 0000000..bda1c11
--- /dev/null
+++ b/migrations/20190927_milestone_texts_not_required.sql
@@ -0,0 +1,8 @@
+--
+-- UPDATE EXISTING TABLES:
+--   milestones
+
+ALTER TABLE milestones ALTER COLUMN "plannedText" DROP NOT NULL;
+ALTER TABLE milestones ALTER COLUMN "activeText" DROP NOT NULL;
+ALTER TABLE milestones ALTER COLUMN "completedText" DROP NOT NULL;
+ALTER TABLE milestones ALTER COLUMN "blockedText" DROP NOT NULL;
\ No newline at end of file
diff --git a/migrations/elasticsearch_sync.js b/migrations/elasticsearch_sync.js
index d5f19ee..9ded699 100644
--- a/migrations/elasticsearch_sync.js
+++ b/migrations/elasticsearch_sync.js
@@ -673,6 +673,7 @@ function getRequestBody(indexName) {
         updateAllTypes: true,
         body: {
           mappings: { },
+          refresh: 'wait_for',
         },
       };
       result.body.mappings[ES_PROJECT_TYPE] = projectMapping;
@@ -683,6 +684,7 @@ function getRequestBody(indexName) {
         updateAllTypes: true,
         body: {
           mappings: { },
+          refresh: 'wait_for',
         },
       };
       result.body.mappings[ES_METADATA_TYPE] = metadataMapping;
@@ -693,6 +695,7 @@ function getRequestBody(indexName) {
         updateAllTypes: true,
         body: {
           mappings: { },
+          refresh: 'wait_for',
         },
       };
       result.body.mappings[ES_TIMELINE_TYPE] = timelineMapping;
@@ -703,24 +706,39 @@ function getRequestBody(indexName) {
   return result;
 }
 
-    // first delete the index if already present
-esClient.indices.delete({
-  index: ES_PROJECT_INDEX,
-  // we would want to ignore no such index error
-  ignore: [404],
-})
-.then(() => esClient.indices.create(getRequestBody(ES_PROJECT_INDEX)))
-// Re-create timeline index
-.then(() => esClient.indices.delete({ index: ES_TIMELINE_INDEX, ignore: [404] }))
-.then(() => esClient.indices.create(getRequestBody(ES_TIMELINE_INDEX)))
-// Re-create metadata index
-.then(() => esClient.indices.delete({ index: ES_METADATA_INDEX, ignore: [404] }))
-.then(() => esClient.indices.create(getRequestBody(ES_METADATA_INDEX)))
-.then(() => {
-  console.log('elasticsearch indices synced successfully');
-  process.exit();
-})
-.catch((err) => {
-  console.error('elasticsearch indices sync failed', err);
-  process.exit();
-});
+/**
+ * Sync elasticsearch indices.
+ *
+ * @returns {undefined}
+ */
+function sync() {
+      // first delete the index if already present
+  return esClient.indices.delete({
+    index: ES_PROJECT_INDEX,
+    // we would want to ignore no such index error
+    ignore: [404],
+  })
+  .then(() => esClient.indices.create(getRequestBody(ES_PROJECT_INDEX)))
+  // Re-create timeline index
+  .then(() => esClient.indices.delete({ index: ES_TIMELINE_INDEX, ignore: [404] }))
+  .then(() => esClient.indices.create(getRequestBody(ES_TIMELINE_INDEX)))
+  // Re-create metadata index
+  .then(() => esClient.indices.delete({ index: ES_METADATA_INDEX, ignore: [404] }))
+  .then(() => esClient.indices.create(getRequestBody(ES_METADATA_INDEX)));
+}
+
+if (!module.parent) {
+  sync()
+    .then(() => {
+      console.log('elasticsearch indices synced successfully');
+      process.exit();
+    })
+    .catch((err) => {
+      console.error('elasticsearch indices sync failed', err);
+      process.exit();
+    });
+}
+
+module.exports = {
+  sync,
+};
diff --git a/src/constants.js b/src/constants.js
index dd4d25f..8b66db6 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -9,6 +9,13 @@ export const PROJECT_STATUS = {
   CANCELLED: 'cancelled',
 };
 
+export const WORKSTREAM_STATUS = {
+  DRAFT: 'draft',
+  REVIEWED: 'reviewed',
+  ACTIVE: 'active',
+  COMPLETED: 'completed',
+  PAUSED: 'paused',
+};
 export const PROJECT_PHASE_STATUS = PROJECT_STATUS;
 
 export const MILESTONE_STATUS = PROJECT_STATUS;
@@ -19,12 +26,20 @@ export const PROJECT_MEMBER_ROLE = {
   CUSTOMER: 'customer',
   COPILOT: 'copilot',
   ACCOUNT_MANAGER: 'account_manager',
+  PROGRAM_MANAGER: 'program_manager',
+  ACCOUNT_EXECUTIVE: 'account_executive',
+  SOLUTION_ARCHITECT: 'solution_architect',
+  PROJECT_MANAGER: 'project_manager',
 };
 
 export const PROJECT_MEMBER_MANAGER_ROLES = [
   PROJECT_MEMBER_ROLE.MANAGER,
   PROJECT_MEMBER_ROLE.OBSERVER,
   PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER,
+  PROJECT_MEMBER_ROLE.ACCOUNT_EXECUTIVE,
+  PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+  PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+  PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
 ];
 
 export const USER_ROLE = {
@@ -34,6 +49,12 @@ export const USER_ROLE = {
   COPILOT: 'Connect Copilot',
   CONNECT_ADMIN: 'Connect Admin',
   COPILOT_MANAGER: 'Connect Copilot Manager',
+  BUSINESS_DEVELOPMENT_REPRESENTATIVE: 'Business Development Representative',
+  PRESALES: 'Presales',
+  ACCOUNT_EXECUTIVE: 'Account Executive',
+  PROGRAM_MANAGER: 'Program Manager',
+  SOLUTION_ARCHITECT: 'Solution Architect',
+  PROJECT_MANAGER: 'Project Manager',
 };
 
 export const ADMIN_ROLES = [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN];
@@ -43,6 +64,13 @@ export const MANAGER_ROLES = [
   USER_ROLE.MANAGER,
   USER_ROLE.TOPCODER_ACCOUNT_MANAGER,
   USER_ROLE.COPILOT_MANAGER,
+  USER_ROLE.BUSINESS_DEVELOPMENT_REPRESENTATIVE,
+  USER_ROLE.PRESALES,
+  USER_ROLE.ACCOUNT_EXECUTIVE,
+
+  USER_ROLE.PROGRAM_MANAGER,
+  USER_ROLE.SOLUTION_ARCHITECT,
+  USER_ROLE.PROJECT_MANAGER,
 ];
 
 export const EVENT = {
@@ -133,12 +161,6 @@ export const BUS_API_EVENT = {
   MILESTONE_TEMPLATE_REMOVED: 'project.notification.delete',
   MILESTONE_TEMPLATE_UPDATED: 'project.notification.update',
 
-  // TC Message Service events
-  TOPIC_CREATED: 'notifications.connect.project.topic.created',
-  TOPIC_UPDATED: 'notifications.connect.project.topic.updated',
-  POST_CREATED: 'notifications.connect.project.post.created',
-  POST_UPDATED: 'notifications.connect.project.post.edited',
-
   // Project Member Invites
   PROJECT_MEMBER_INVITE_CREATED: 'project.notification.create',
   PROJECT_MEMBER_INVITE_UPDATED: 'project.notification.update',
@@ -150,6 +172,86 @@ export const BUS_API_EVENT = {
   PROJECT_METADATA_DELETE: 'project.notification.delete',
 };
 
+export const CONNECT_NOTIFICATION_EVENT = {
+  PROJECT_CREATED: 'connect.notification.project.created',
+  PROJECT_UPDATED: 'connect.notification.project.updated',
+  PROJECT_SUBMITTED_FOR_REVIEW: 'connect.notification.project.submittedForReview',
+  PROJECT_APPROVED: 'connect.notification.project.approved',
+  PROJECT_PAUSED: 'connect.notification.project.paused',
+  PROJECT_COMPLETED: 'connect.notification.project.completed',
+  PROJECT_CANCELED: 'connect.notification.project.canceled',
+  PROJECT_ACTIVE: 'connect.notification.project.active',
+
+  PROJECT_PHASE_TRANSITION_ACTIVE: 'connect.notification.project.phase.transition.active',
+  PROJECT_PHASE_TRANSITION_COMPLETED: 'connect.notification.project.phase.transition.completed',
+  PROJECT_PHASE_UPDATE_PAYMENT: 'connect.notification.project.phase.update.payment',
+  PROJECT_PHASE_UPDATE_PROGRESS: 'connect.notification.project.phase.update.progress',
+  PROJECT_PHASE_UPDATE_SCOPE: 'connect.notification.project.phase.update.scope',
+
+  PROJECT_WORK_TRANSITION_ACTIVE: 'connect.notification.project.work.transition.active',
+  PROJECT_WORK_TRANSITION_COMPLETED: 'connect.notification.project.work.transition.completed',
+  PROJECT_WORK_UPDATE_PAYMENT: 'connect.notification.project.work.update.payment',
+  PROJECT_WORK_UPDATE_PROGRESS: 'connect.notification.project.work.update.progress',
+  PROJECT_WORK_UPDATE_SCOPE: 'connect.notification.project.work.update.scope',
+
+  MEMBER_JOINED: 'connect.notification.project.member.joined',
+  MEMBER_LEFT: 'connect.notification.project.member.left',
+  MEMBER_REMOVED: 'connect.notification.project.member.removed',
+  MEMBER_ASSIGNED_AS_OWNER: 'connect.notification.project.member.assignedAsOwner',
+  MEMBER_JOINED_COPILOT: 'connect.notification.project.member.copilotJoined',
+  MEMBER_JOINED_MANAGER: 'connect.notification.project.member.managerJoined',
+
+  PROJECT_LINK_CREATED: 'connect.notification.project.linkCreated',
+  PROJECT_FILE_UPLOADED: 'connect.notification.project.fileUploaded',
+  PROJECT_SPECIFICATION_MODIFIED: 'connect.notification.project.updated.spec',
+  PROJECT_PROGRESS_MODIFIED: 'connect.notification.project.updated.progress',
+  PROJECT_FILES_UPDATED: 'connect.notification.project.files.updated',
+  PROJECT_TEAM_UPDATED: 'connect.notification.project.team.updated',
+
+  // When phase is added/updated/deleted from the project,
+  // When product is added/deleted from a phase
+  // When product is updated on any field other than specification
+  PROJECT_PLAN_UPDATED: 'connect.notification.project.plan.updated',
+
+  PROJECT_PLAN_READY: 'connect.notification.project.plan.ready',
+
+  // When milestone is added/deleted to/from the phase,
+  // When milestone is updated for duration/startDate/endDate/status
+  TIMELINE_ADJUSTED: 'connect.notification.project.timeline.adjusted',
+
+  // When specification of a product is modified
+  PROJECT_PRODUCT_SPECIFICATION_MODIFIED: 'connect.notification.project.product.update.spec',
+
+  // When specification of a work item is modified
+  PROJECT_WORKITEM_SPECIFICATION_MODIFIED: 'connect.notification.project.workitem.update.spec',
+
+  MILESTONE_ADDED: 'connect.notification.project.timeline.milestone.added',
+  MILESTONE_REMOVED: 'connect.notification.project.timeline.milestone.removed',
+  MILESTONE_UPDATED: 'connect.notification.project.timeline.milestone.updated',
+  // When milestone is marked as active
+  MILESTONE_TRANSITION_ACTIVE: 'connect.notification.project.timeline.milestone.transition.active',
+  // When milestone is marked as completed
+  MILESTONE_TRANSITION_COMPLETED: 'connect.notification.project.timeline.milestone.transition.completed',
+   // When milestone is marked as paused
+  MILESTONE_TRANSITION_PAUSED: 'connect.notification.project.timeline.milestone.transition.paused',
+  // When milestone is waiting for customers's input
+  MILESTONE_WAITING_CUSTOMER: 'connect.notification.project.timeline.milestone.waiting.customer',
+
+  // Project Member Invites
+  PROJECT_MEMBER_INVITE_CREATED: 'connect.notification.project.member.invite.created',
+  PROJECT_MEMBER_INVITE_REQUESTED: 'connect.notification.project.member.invite.requested',
+  PROJECT_MEMBER_INVITE_UPDATED: 'connect.notification.project.member.invite.updated',
+  PROJECT_MEMBER_INVITE_APPROVED: 'connect.notification.project.member.invite.approved',
+  PROJECT_MEMBER_INVITE_REJECTED: 'connect.notification.project.member.invite.rejected',
+  PROJECT_MEMBER_EMAIL_INVITE_CREATED: 'connect.notification.email.project.member.invite.created',
+
+  // TC Message Service events
+  TOPIC_CREATED: 'connect.notification.project.topic.created',
+  TOPIC_UPDATED: 'connect.notification.project.topic.updated',
+  POST_CREATED: 'connect.notification.project.post.created',
+  POST_UPDATED: 'connect.notification.project.post.edited',
+};
+
 export const REGEX = {
   URL: /^(http(s?):\/\/)?(www\.)?[a-zA-Z0-9\.\-\_]+(\.[a-zA-Z]{2,15})+(\:[0-9]{2,5})?(\/[a-zA-Z0-9\_\-\s\.\/\?\%\#\&\=;]*)?$/, // eslint-disable-line
 };
@@ -162,6 +264,11 @@ export const TIMELINE_REFERENCES = {
   PROJECT: 'project',
   PHASE: 'phase',
   PRODUCT: 'product',
+  WORK: 'work',
+};
+
+export const STATUS_HISTORY_REFERENCES = {
+  MILESTONE: 'milestone',
 };
 
 export const MILESTONE_TEMPLATE_REFERENCES = {
@@ -178,6 +285,44 @@ export const INVITE_STATUS = {
   CANCELED: 'canceled',
 };
 
+export const SCOPE_CHANGE_REQ_STATUS = {
+  PENDING: 'pending',
+  APPROVED: 'approved',
+  REJECTED: 'rejected',
+  ACTIVATED: 'activated',
+  CANCELED: 'canceled',
+};
+export const MAX_PARALLEL_REQUEST_QTY = 10;
+
+export const ROUTES = {
+  PHASE_PRODUCTS: {
+    UPDATE: 'phase_products.update',
+  },
+  PHASES: {
+    UPDATE: 'phases.update',
+  },
+  WORKS: {
+    UPDATE: 'works.update',
+  },
+  WORK_ITEMS: {
+    UPDATE: 'work_items.update',
+  },
+};
+
+export const ESTIMATION_TYPE = {
+  FEE: 'fee',
+  COMMUNITY: 'community',
+  TOPCODER_SERVICE: 'topcoder_service',
+};
+
+export const VALUE_TYPE = {
+  INT: 'int',
+  DOUBLE: 'double',
+  STRING: 'string',
+  PERCENTAGE: 'percentage',
+};
+
+
 export const RESOURCES = {
   PROJECT: 'project',
   PROJECT_TEMPLATE: 'project.template',
diff --git a/src/events/busApi.js b/src/events/busApi.js
index 1dac212..3bc92ca 100644
--- a/src/events/busApi.js
+++ b/src/events/busApi.js
@@ -1,7 +1,44 @@
 import _ from 'lodash';
+import moment from 'moment';
 import config from 'config';
-import { EVENT, BUS_API_EVENT } from '../constants';
+import {
+  EVENT,
+  BUS_API_EVENT,
+  CONNECT_NOTIFICATION_EVENT,
+  PROJECT_STATUS,
+  PROJECT_PHASE_STATUS,
+  PROJECT_MEMBER_ROLE,
+  ROUTES,
+  MILESTONE_STATUS,
+  INVITE_STATUS,
+} from '../constants';
 import { createEvent } from '../services/busApi';
+import models from '../models';
+import util from '../util';
+
+/**
+ * Map of project status and event name sent to bus api
+ */
+const mapEventTypes = {
+  [PROJECT_STATUS.DRAFT]: CONNECT_NOTIFICATION_EVENT.PROJECT_CREATED,
+  [PROJECT_STATUS.IN_REVIEW]: CONNECT_NOTIFICATION_EVENT.PROJECT_SUBMITTED_FOR_REVIEW,
+  [PROJECT_STATUS.REVIEWED]: CONNECT_NOTIFICATION_EVENT.PROJECT_APPROVED,
+  [PROJECT_STATUS.COMPLETED]: CONNECT_NOTIFICATION_EVENT.PROJECT_COMPLETED,
+  [PROJECT_STATUS.CANCELLED]: CONNECT_NOTIFICATION_EVENT.PROJECT_CANCELED,
+  [PROJECT_STATUS.PAUSED]: CONNECT_NOTIFICATION_EVENT.PROJECT_PAUSED,
+  [PROJECT_STATUS.ACTIVE]: CONNECT_NOTIFICATION_EVENT.PROJECT_ACTIVE,
+};
+
+/**
+ * Builds the connect project attachment url for the given project and attachment ids.
+ *
+ * @param {string|number} projectId the project id
+ * @param {string|number} attachmentId the attachment id
+ * @returns {string} the connect project attachment url
+ */
+function connectProjectAttachmentUrl(projectId, attachmentId) {
+  return `${config.get('connectProjectsUrl')}${projectId}/attachments/${attachmentId}`;
+}
 
 /**
  * Builds the connect project url for the given project id.
@@ -17,7 +54,7 @@ module.exports = (app, logger) => {
   /**
    * PROJECT_DRAFT_CREATED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, ({ req, project }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_DRAFT_CREATED, ({ req, project }) => {
     logger.debug('receive PROJECT_DRAFT_CREATED event');
 
     // send event to bus api
@@ -25,18 +62,82 @@ module.exports = (app, logger) => {
       refCode: _.get(project, 'details.utm.code'),
       projectUrl: connectProjectUrl(project.id),
     }), logger);
+
+    /*
+      Send event for Notification Service
+     */
+    createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_CREATED, {
+      projectId: project.id,
+      projectName: project.name,
+      refCode: _.get(project, 'details.utm.code'),
+      projectUrl: connectProjectUrl(project.id),
+      userId: req.authUser.userId,
+      initiatorUserId: req.authUser.userId,
+    }, logger);
   });
 
   /**
    * PROJECT_UPDATED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_UPDATED, ({ req, updated }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_UPDATED, ({ req, original, updated }) => {
     logger.debug('receive PROJECT_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_UPDATED, _.assign(updated, {
       refCode: _.get(updated, 'details.utm.code'),
       projectUrl: connectProjectUrl(updated.id),
     }), logger);
+
+    /*
+      Send event for Notification Service
+     */
+    if (original.status !== updated.status) {
+      logger.debug(`project status is updated from ${original.status} to ${updated.status}`);
+      createEvent(mapEventTypes[updated.status], {
+        projectId: updated.id,
+        projectName: updated.name,
+        refCode: _.get(updated, 'details.utm.code'),
+        projectUrl: connectProjectUrl(updated.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    } else if (
+      !_.isEqual(original.details, updated.details) ||
+      !_.isEqual(original.name, updated.name) ||
+      !_.isEqual(original.description, updated.description)) {
+      logger.debug('project spec is updated');
+      createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_SPECIFICATION_MODIFIED, {
+        projectId: updated.id,
+        projectName: updated.name,
+        refCode: _.get(updated, 'details.utm.code'),
+        projectUrl: connectProjectUrl(updated.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    } else if (!_.isEqual(original.bookmarks, updated.bookmarks)) {
+      logger.debug('project bookmarks is updated');
+      createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_LINK_CREATED, {
+        projectId: updated.id,
+        projectName: updated.name,
+        refCode: _.get(updated, 'details.utm.code'),
+        projectUrl: connectProjectUrl(updated.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    }
+
+    // send PROJECT_UPDATED Kafka message when one of the specified below properties changed
+    const watchProperties = ['status', 'details', 'name', 'description', 'bookmarks'];
+    if (!_.isEqual(_.pick(original, watchProperties),
+                   _.pick(updated, watchProperties))) {
+      createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, {
+        projectId: updated.id,
+        projectName: updated.name,
+        refCode: _.get(updated, 'details.utm.code'),
+        projectUrl: connectProjectUrl(updated.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    }
   });
 
   /**
@@ -79,84 +180,486 @@ module.exports = (app, logger) => {
   /**
    * PROJECT_MEMBER_ADDED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED, ({ req, resource }) => {
     logger.debug('receive PROJECT_MEMBER_ADDED event');
 
     createEvent(BUS_API_EVENT.PROJECT_MEMBER_ADDED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    let eventType;
+    const member = _.omit(resource, 'resource');
+
+    if ([
+      PROJECT_MEMBER_ROLE.MANAGER,
+      PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+      PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+      PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+    ].includes(member.role)) {
+      eventType = CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED_MANAGER;
+    } else if (member.role === PROJECT_MEMBER_ROLE.COPILOT) {
+      eventType = CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED_COPILOT;
+    } else {
+      eventType = CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED;
+    }
+    const projectId = _.parseInt(req.params.projectId);
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        createEvent(eventType, {
+          projectId,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(projectId),
+          userId: member.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, {
+          projectId: project.id,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(project.id),
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
    * PROJECT_MEMBER_REMOVED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED, ({ req, resource }) => {
     logger.debug('receive PROJECT_MEMBER_REMOVED event');
 
     createEvent(BUS_API_EVENT.PROJECT_MEMBER_REMOVED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    let eventType;
+    const member = _.omit(resource, 'resource');
+    if (member.userId === req.authUser.userId) {
+      eventType = CONNECT_NOTIFICATION_EVENT.MEMBER_LEFT;
+    } else {
+      eventType = CONNECT_NOTIFICATION_EVENT.MEMBER_REMOVED;
+    }
+    const projectId = _.parseInt(req.params.projectId);
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        if (project) {
+          createEvent(eventType, {
+            projectId,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(projectId),
+            userId: member.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+
+          createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, {
+            projectId: project.id,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(project.id),
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
    * PROJECT_MEMBER_UPDATED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, ({ req, resource }) => {    // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED, ({ req, resource, originalResource }) => {
     logger.debug('receive PROJECT_MEMBER_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_MEMBER_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const updated = _.omit(resource, 'resource');
+    const original = _.omit(originalResource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        if (project) {
+          if (updated.isPrimary && !original.isPrimary) {
+            createEvent(CONNECT_NOTIFICATION_EVENT.MEMBER_ASSIGNED_AS_OWNER, {
+              projectId,
+              projectName: project.name,
+              refCode: _.get(project, 'details.utm.code'),
+              projectUrl: connectProjectUrl(projectId),
+              userId: updated.userId,
+              initiatorUserId: req.authUser.userId,
+            }, logger);
+          }
+
+          createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, {
+            projectId: project.id,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(project.id),
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
    * PROJECT_ATTACHMENT_ADDED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_ADDED, ({ req, resource }) => {
     logger.debug('receive PROJECT_ATTACHMENT_ADDED event');
 
     createEvent(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const attachment = _.omit(resource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_FILE_UPLOADED, {
+          projectId,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(projectId),
+          fileName: attachment.filePath.replace(/^.*[\\\/]/, ''),    // eslint-disable-line
+          fileUrl: connectProjectAttachmentUrl(projectId, attachment.id),
+          allowedUsers: attachment.allowedUsers,
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, {
+          projectId: project.id,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(project.id),
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
    * PROJECT_ATTACHMENT_UPDATED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_UPDATED, ({ req, resource }) => {
     logger.debug('receive PROJECT_ATTACHMENT_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_ATTACHMENT_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+    .then((project) => {
+      createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, {
+        projectId: project.id,
+        projectName: project.name,
+        refCode: _.get(project, 'details.utm.code'),
+        projectUrl: connectProjectUrl(project.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
    * PROJECT_ATTACHMENT_REMOVED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED, ({ req, resource }) => {
     logger.debug('receive PROJECT_ATTACHMENT_REMOVED event');
 
     createEvent(BUS_API_EVENT.PROJECT_ATTACHMENT_REMOVED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+    .then((project) => {
+      createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, {
+        projectId: project.id,
+        projectName: project.name,
+        refCode: _.get(project, 'details.utm.code'),
+        projectUrl: connectProjectUrl(project.id),
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
+  /**
+   * If the project is in draft status and the phase is in reviewed status, and it's the
+   * only phase in the project with that status, then send the plan ready event.
+   *
+   * @param {object} req the req
+   * @param {object} project the project
+   * @param {object} phase the phase that was created/updated
+   * @returns {Promise<void>} void
+   */
+  async function sendPlanReadyEventIfNeeded(req, project, phase) {
+    if (project.status === PROJECT_STATUS.DRAFT &&
+      phase.status === PROJECT_PHASE_STATUS.REVIEWED) {
+      await models.ProjectPhase.count({
+        where: { projectId: project.id, status: PROJECT_PHASE_STATUS.REVIEWED },
+      }).then(((count) => {
+        // only send the plan ready event when this is the only reviewed phase in the project
+        if (count === 1) {
+          createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_READY, {
+            projectId: project.id,
+            phaseId: phase.id,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+      }));
+    }
+  }
+
   /**
    * PROJECT_PHASE_ADDED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED, ({ req, resource }) => {
     logger.debug('receive PROJECT_PHASE_ADDED event');
 
     createEvent(BUS_API_EVENT.PROJECT_PHASE_CREATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const created = _.omit(resource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, {
+          projectId,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(projectId),
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+          allowedUsers: created.status === PROJECT_PHASE_STATUS.DRAFT ?
+            util.getTopcoderProjectMembers(project.members) : null,
+        }, logger);
+        return sendPlanReadyEventIfNeeded(req, project, created);
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
   * PROJECT_PHASE_REMOVED
   */
-  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED, ({ req, resource }) => {
     logger.debug('receive PROJECT_PHASE_REMOVED event');
 
     createEvent(BUS_API_EVENT.PROJECT_PHASE_DELETED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const deleted = _.omit(resource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, {
+          projectId,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(projectId),
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+          allowedUsers: deleted.status === PROJECT_PHASE_STATUS.DRAFT ?
+          util.getTopcoderProjectMembers(project.members) : null,
+        }, logger);
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
   * PROJECT_PHASE_UPDATED
   */
-  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, ({ req, resource, originalResource, route, skipNotification }) => { // eslint-disable-line no-unused-vars
     logger.debug('receive PROJECT_PHASE_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_PHASE_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    if (!skipNotification) {
+      const projectId = _.parseInt(req.params.projectId);
+      const phaseId = _.parseInt(req.params.phaseId);
+      const updated = _.omit(resource, 'resource');
+      const original = _.omit(originalResource, 'resource');
+
+      models.Project.findOne({
+        where: { id: projectId },
+      })
+        .then((project) => {
+          logger.debug(`Fetched project ${projectId} for the phase ${phaseId}`);
+          const eventsMap = {};
+          [
+            ['duration', CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED],
+            ['startDate', CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED],
+            ['spentBudget', route === ROUTES.PHASES.UPDATE
+                ? CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_PAYMENT
+                : CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_PAYMENT,
+            ],
+            ['progress', [route === ROUTES.PHASES.UPDATE
+                ? CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_PROGRESS
+                : CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_PROGRESS,
+              CONNECT_NOTIFICATION_EVENT.PROJECT_PROGRESS_MODIFIED,
+            ]],
+            ['details', route === ROUTES.PHASES.UPDATE
+                ? CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_SCOPE
+                : CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_SCOPE,
+            ],
+            ['status', route === ROUTES.PHASES.UPDATE
+                ? CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_TRANSITION_ACTIVE
+                : CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_TRANSITION_ACTIVE,
+              PROJECT_PHASE_STATUS.ACTIVE,
+            ],
+            ['status', route === ROUTES.PHASES.UPDATE
+                ? CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_TRANSITION_COMPLETED
+                : CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_TRANSITION_COMPLETED,
+              PROJECT_PHASE_STATUS.COMPLETED,
+            ],
+            // ideally we should validate the old value being 'DRAFT' but there is no other status from which
+            // we can move phase to REVIEWED status
+            ['status', CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, PROJECT_PHASE_STATUS.REVIEWED],
+            // ideally we should validate the old value being 'REVIEWED' but there is no other status from which
+            // we can move phase to DRAFT status
+            ['status', CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, PROJECT_PHASE_STATUS.DRAFT],
+          ].forEach(([key, events, sendIfNewEqual]) => {
+            // eslint-disable-next-line no-param-reassign
+            events = Array.isArray(events) ? events : [events];
+            // eslint-disable-next-line no-param-reassign
+            events = _.filter(events, e => !eventsMap[e]);
+
+            // send event(s) only if the target field's value was updated, or when an update matches a "sendIfNewEqual" value
+            if ((!sendIfNewEqual && !_.isEqual(original[key], updated[key])) ||
+              (original[key] !== sendIfNewEqual && updated[key] === sendIfNewEqual)) {
+              events.forEach(event => createEvent(event, {
+                projectId,
+                phaseId,
+                projectUrl: connectProjectUrl(projectId),
+                originalPhase: original,
+                updatedPhase: updated,
+                projectName: project.name,
+                userId: req.authUser.userId,
+                initiatorUserId: req.authUser.userId,
+                allowedUsers: updated.status === PROJECT_PHASE_STATUS.DRAFT ?
+                util.getTopcoderProjectMembers(project.members) : null,
+              }, logger));
+              events.forEach((event) => { eventsMap[event] = true; });
+            }
+          });
+
+          return sendPlanReadyEventIfNeeded(req, project, updated);
+        }).catch(err => null);    // eslint-disable-line no-unused-vars
+    }
   });
 
+  /**
+   * Send milestone notification if needed.
+   * @param {Object} req the request
+   * @param {Object} original the original milestone
+   * @param {Object} updated the updated milestone
+   * @param {Object} project the project
+   * @param {Object} timeline the updated timeline
+   * @returns {Promise<void>} void
+   */
+  function sendMilestoneNotification(req, original, updated, project, timeline) {
+    logger.debug('sendMilestoneNotification', original, updated);
+    // throw generic milestone updated bus api event
+    createEvent(CONNECT_NOTIFICATION_EVENT.MILESTONE_UPDATED, {
+      projectId: project.id,
+      projectName: project.name,
+      refCode: _.get(project, 'details.utm.code'),
+      projectUrl: connectProjectUrl(project.id),
+      timeline,
+      originalMilestone: original,
+      updatedMilestone: updated,
+      userId: req.authUser.userId,
+      initiatorUserId: req.authUser.userId,
+    }, logger);
+    // Send transition events
+    if (original.status !== updated.status) {
+      let event;
+      if (updated.status === MILESTONE_STATUS.COMPLETED) {
+        event = CONNECT_NOTIFICATION_EVENT.MILESTONE_TRANSITION_COMPLETED;
+      } else if (updated.status === MILESTONE_STATUS.ACTIVE) {
+        event = CONNECT_NOTIFICATION_EVENT.MILESTONE_TRANSITION_ACTIVE;
+      } else if (updated.status === MILESTONE_STATUS.PAUSED) {
+        event = CONNECT_NOTIFICATION_EVENT.MILESTONE_TRANSITION_PAUSED;
+      }
+
+      if (event) {
+        createEvent(event, {
+          projectId: project.id,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(project.id),
+          timeline,
+          originalMilestone: original,
+          updatedMilestone: updated,
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+      }
+    }
+
+    // Send notifications.connect.project.phase.milestone.waiting.customer event
+    const originalWaiting = _.get(original, 'details.metadata.waitingForCustomer', false);
+    const updatedWaiting = _.get(updated, 'details.metadata.waitingForCustomer', false);
+    if (!originalWaiting && updatedWaiting) {
+      createEvent(CONNECT_NOTIFICATION_EVENT.MILESTONE_WAITING_CUSTOMER, {
+        projectId: project.id,
+        projectName: project.name,
+        refCode: _.get(project, 'details.utm.code'),
+        projectUrl: connectProjectUrl(project.id),
+        timeline,
+        originalMilestone: original,
+        updatedMilestone: updated,
+        userId: req.authUser.userId,
+        initiatorUserId: req.authUser.userId,
+      }, logger);
+    }
+  }
+
   /**
    * MILESTONE_ADDED.
    */
@@ -164,15 +667,95 @@ module.exports = (app, logger) => {
     logger.debug('receive MILESTONE_ADDED event');
 
     createEvent(BUS_API_EVENT.MILESTONE_ADDED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const created = _.omit(resource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        if (project) {
+          createEvent(CONNECT_NOTIFICATION_EVENT.MILESTONE_ADDED, {
+            projectId,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(projectId),
+            addedMilestone: created,
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+        // sendMilestoneNotification(req, {}, created, project);
+      })
+      .catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
   * MILESTONE_UPDATED.
   */
-  app.on(EVENT.ROUTING_KEY.MILESTONE_UPDATED, ({ req, resource }) => {  // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.MILESTONE_UPDATED, ({
+    req,
+    resource,
+    originalResource,
+    cascadedUpdates,
+    skipNotification,
+  }) => {  // eslint-disable-line no-unused-vars
     logger.debug(`receive MILESTONE_UPDATED event for milestone ${resource.id}`);
 
     createEvent(BUS_API_EVENT.MILESTONE_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    if (!skipNotification) {
+      const projectId = _.parseInt(req.params.projectId);
+      const timeline = _.omit(req.timeline.toJSON(), 'deletedAt', 'deletedBy');
+      const updated = _.omit(resource, 'resource');
+      const original = _.omit(originalResource, 'resource');
+
+      models.Project.findOne({
+        where: { id: projectId },
+      })
+      .then((project) => {
+        logger.debug(`Found project with id ${projectId}`);
+        return models.Milestone.getTimelineDuration(timeline.id)
+        .then(({ duration, progress }) => {
+          timeline.duration = duration;
+          timeline.progress = progress;
+          sendMilestoneNotification(req, original, updated, project, timeline);
+
+          logger.debug('cascadedUpdates', cascadedUpdates);
+          if (cascadedUpdates && cascadedUpdates.milestones && cascadedUpdates.milestones.length > 0) {
+            _.each(cascadedUpdates.milestones, cascadedUpdate =>
+              sendMilestoneNotification(req, cascadedUpdate.original, cascadedUpdate.updated, project, timeline),
+            );
+          }
+
+          // if timeline is modified
+          if (cascadedUpdates && cascadedUpdates.timeline) {
+            const cTimeline = cascadedUpdates.timeline;
+            // if endDate of the timeline is modified, raise TIMELINE_ADJUSTED event
+            if (!moment(cTimeline.original.endDate).isSame(cTimeline.updated.endDate)) {
+              // Raise Timeline changed event
+              createEvent(CONNECT_NOTIFICATION_EVENT.TIMELINE_ADJUSTED, {
+                projectId: project.id,
+                projectName: project.name,
+                refCode: _.get(project, 'details.utm.code'),
+                projectUrl: connectProjectUrl(project.id),
+                originalTimeline: cTimeline.original,
+                updatedTimeline: cTimeline.updated,
+                userId: req.authUser.userId,
+                initiatorUserId: req.authUser.userId,
+              }, logger);
+            }
+          }
+        });
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
+    }
   });
 
  /**
@@ -182,6 +765,29 @@ module.exports = (app, logger) => {
     logger.debug('receive MILESTONE_REMOVED event');
 
     createEvent(BUS_API_EVENT.MILESTONE_REMOVED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const deleted = _.omit(resource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+    .then((project) => {
+      if (project) {
+        createEvent(CONNECT_NOTIFICATION_EVENT.MILESTONE_REMOVED, {
+          projectId,
+          projectName: project.name,
+          refCode: _.get(project, 'details.utm.code'),
+          projectUrl: connectProjectUrl(projectId),
+          removedMilestone: deleted,
+          userId: req.authUser.userId,
+          initiatorUserId: req.authUser.userId,
+        }, logger);
+      }
+    }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
@@ -232,10 +838,41 @@ module.exports = (app, logger) => {
   /**
    * TIMELINE_UPDATED
    */
-  app.on(EVENT.ROUTING_KEY.TIMELINE_UPDATED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.TIMELINE_UPDATED, ({ req, resource, originalResource }) => { // eslint-disable-line no-unused-vars
     logger.debug('receive TIMELINE_UPDATED event');
 
     createEvent(BUS_API_EVENT.TIMELINE_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const updated = _.omit(resource, 'resource');
+    const original = _.omit(originalResource, 'resource');
+    // send PROJECT_UPDATED Kafka message when one of the specified below properties changed
+    const watchProperties = ['startDate', 'endDate'];
+    if (!_.isEqual(_.pick(original, watchProperties),
+                   _.pick(updated, watchProperties))) {
+      // req.params.projectId is set by validateTimelineIdParam middleware
+      const projectId = _.parseInt(req.params.projectId);
+
+      models.Project.findOne({
+        where: { id: projectId },
+      })
+      .then((project) => {
+        if (project) {
+          createEvent(CONNECT_NOTIFICATION_EVENT.TIMELINE_ADJUSTED, {
+            projectId,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(projectId),
+            originalTimeline: original,
+            updatedTimeline: updated,
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
+    }
   });
 
   /**
@@ -259,10 +896,55 @@ module.exports = (app, logger) => {
   /**
    * PROJECT_PHASE_PRODUCT_UPDATED
    */
-  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED, ({ req, resource }) => { // eslint-disable-line no-unused-vars
+  app.on(EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED, ({ req, resource, originalResource, route }) => { // eslint-disable-line no-unused-vars
     logger.debug('receive PROJECT_PHASE_PRODUCT_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const updated = _.omit(resource, 'resource');
+    const original = _.omit(originalResource, 'resource');
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+      .then((project) => {
+        // Spec changes
+        if (!_.isEqual(original.details, updated.details)) {
+          logger.debug(`Spec changed for product id ${updated.id}`);
+
+          const busApiEvent = route === ROUTES.PHASE_PRODUCTS.UPDATE
+              ? CONNECT_NOTIFICATION_EVENT.PROJECT_PRODUCT_SPECIFICATION_MODIFIED
+              : CONNECT_NOTIFICATION_EVENT.PROJECT_WORKITEM_SPECIFICATION_MODIFIED;
+
+          createEvent(busApiEvent, {
+            projectId,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(projectId),
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+          }, logger);
+        }
+
+        const watchProperties = ['name', 'estimatedPrice', 'actualPrice', 'details'];
+        if (!_.isEqual(_.pick(original, watchProperties),
+                       _.pick(updated, watchProperties))) {
+          createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, {
+            projectId,
+            projectName: project.name,
+            refCode: _.get(project, 'details.utm.code'),
+            projectUrl: connectProjectUrl(projectId),
+            userId: req.authUser.userId,
+            initiatorUserId: req.authUser.userId,
+            allowedUsers: updated.status === PROJECT_PHASE_STATUS.DRAFT ?
+            util.getTopcoderProjectMembers(project.members) : null,
+          }, logger);
+        }
+      }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
@@ -272,6 +954,50 @@ module.exports = (app, logger) => {
     logger.debug('receive PROJECT_MEMBER_INVITE_CREATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const userId = resource.userId;
+    const email = resource.email;
+    const status = resource.status;
+    const role = resource.role;
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+    .then((project) => {
+      logger.debug(util.isSSO);
+      if (status === INVITE_STATUS.REQUESTED) {
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_REQUESTED, {
+          projectId,
+          userId,
+          email,
+          role,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        }, logger);
+      } else {
+        // send event to bus api
+        logger.debug({
+          projectId,
+          userId,
+          email,
+          role,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        });
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_CREATED, {
+          projectId,
+          userId,
+          email,
+          role,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        }, logger);
+      }
+    }).catch(err => logger.error(err));    // eslint-disable-line no-unused-vars
   });
 
   /**
@@ -281,6 +1007,59 @@ module.exports = (app, logger) => {
     logger.debug('receive PROJECT_MEMBER_INVITE_UPDATED event');
 
     createEvent(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED, resource, logger);
+
+    /*
+      Send event for Notification Service
+     */
+    const projectId = _.parseInt(req.params.projectId);
+    const userId = resource.userId;
+    const email = resource.email;
+    const status = resource.status;
+    const role = resource.role;
+    const createdBy = resource.createdBy;
+
+    models.Project.findOne({
+      where: { id: projectId },
+    })
+    .then((project) => {
+      logger.debug(util.isSSO);
+      if (status === INVITE_STATUS.REQUEST_APPROVED) {
+        // send event to bus api
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_APPROVED, {
+          projectId,
+          userId,
+          originator: createdBy,
+          email,
+          role,
+          status,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        }, logger);
+      } else if (status === INVITE_STATUS.REQUEST_REJECTED) {
+        // send event to bus api
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_REJECTED, {
+          projectId,
+          userId,
+          originator: createdBy,
+          email,
+          role,
+          status,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        }, logger);
+      } else {
+        // send event to bus api
+        createEvent(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_UPDATED, {
+          projectId,
+          userId,
+          email,
+          role,
+          status,
+          initiatorUserId: req.authUser.userId,
+          isSSO: util.isSSO(project),
+        }, logger);
+      }
+    }).catch(err => null);    // eslint-disable-line no-unused-vars
   });
 
   /**
diff --git a/src/events/index.js b/src/events/index.js
index 67b1b15..e161ef6 100644
--- a/src/events/index.js
+++ b/src/events/index.js
@@ -1,5 +1,5 @@
 
-import { EVENT, BUS_API_EVENT } from '../constants';
+import { EVENT, CONNECT_NOTIFICATION_EVENT } from '../constants';
 import { projectCreatedHandler, projectUpdatedHandler, projectDeletedHandler,
   projectUpdatedKafkaHandler } from './projects';
 import { projectMemberAddedHandler, projectMemberRemovedHandler,
@@ -41,9 +41,6 @@ export const rabbitHandlers = {
   [EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED]: projectPhaseAddedHandler,
   [EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED]: projectPhaseRemovedHandler,
   [EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED]: projectPhaseUpdatedHandler,
-  [EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED]: projectPhaseAddedHandler,
-  [EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED]: projectPhaseRemovedHandler,
-  [EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED]: projectPhaseUpdatedHandler,
   [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED]: phaseProductAddedHandler,
   [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED]: phaseProductRemovedHandler,
   [EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED]: phaseProductUpdatedHandler,
@@ -60,18 +57,18 @@ export const rabbitHandlers = {
 
 export const kafkaHandlers = {
   // Events defined by project-api
-  [BUS_API_EVENT.PROJECT_UPDATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.PROJECT_FILES_UPDATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.PROJECT_TEAM_UPDATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.PROJECT_PLAN_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED]: projectUpdatedKafkaHandler,
 
   // Events from message-service
-  [BUS_API_EVENT.TOPIC_CREATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.TOPIC_UPDATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.POST_CREATED]: projectUpdatedKafkaHandler,
-  [BUS_API_EVENT.POST_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.TOPIC_CREATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.TOPIC_UPDATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.POST_CREATED]: projectUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.POST_UPDATED]: projectUpdatedKafkaHandler,
 
   // Events coming from timeline/milestones (considering it as a separate module/service in future)
-  [BUS_API_EVENT.MILESTONE_TRANSITION_COMPLETED]: milestoneUpdatedKafkaHandler,
-  [BUS_API_EVENT.TIMELINE_ADJUSTED]: timelineAdjustedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.MILESTONE_TRANSITION_COMPLETED]: milestoneUpdatedKafkaHandler,
+  [CONNECT_NOTIFICATION_EVENT.TIMELINE_ADJUSTED]: timelineAdjustedKafkaHandler,
 };
diff --git a/src/events/milestones/index.js b/src/events/milestones/index.js
index d8f884a..8b71766 100644
--- a/src/events/milestones/index.js
+++ b/src/events/milestones/index.js
@@ -7,7 +7,7 @@ import Joi from 'joi';
 import Promise from 'bluebird';
 import util from '../../util';
 // import { createEvent } from '../../services/busApi';
-import { EVENT, TIMELINE_REFERENCES, MILESTONE_STATUS, REGEX } from '../../constants';
+import { EVENT, TIMELINE_REFERENCES, MILESTONE_STATUS, REGEX, RESOURCES, ROUTES } from '../../constants';
 import models from '../../models';
 
 const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName');
@@ -82,35 +82,14 @@ const milestoneUpdatedHandler = Promise.coroutine(function* (logger, msg, channe
       });
     }
 
-    // if (data.original.order !== data.updated.order) {
-    //   const milestoneWithSameOrder =
-    //     _.find(milestones, milestone => milestone.id !== data.updated.id && milestone.order === data.updated.order);
-    //   if (milestoneWithSameOrder) {
-    //     // Increase the order from M to K: if there is an item with order K,
-    //     // orders from M+1 to K should be made M to K-1
-    //     if (data.original.order < data.updated.order) {
-    //       _.each(milestones, (single) => {
-    //         if (single.id !== data.updated.id
-    //           && (data.original.order + 1) <= single.order
-    //           && single.order <= data.updated.order) {
-    //           single.order -= 1; // eslint-disable-line no-param-reassign
-    //         }
-    //       });
-    //     } else {
-    //       // Decrease the order from M to K: if there is an item with order K,
-    //       // orders from K to M-1 should be made K+1 to M
-    //       _.each(milestones, (single) => {
-    //         if (single.id !== data.updated.id
-    //           && data.updated.order <= single.order
-    //           && single.order <= (data.original.order - 1)) {
-    //           single.order += 1; // eslint-disable-line no-param-reassign
-    //         }
-    //       });
-    //     }
-    //   }
-    // }
+    let updatedTimeline = doc._source; // eslint-disable-line no-underscore-dangle
+    // if timeline has been modified during milestones updates
+    if (data.cascadedUpdates && data.cascadedUpdates.timeline && data.cascadedUpdates.timeline.updated) {
+      // merge updated timeline with the object in ES index, the same way as we do when updating timeline in ES using timeline endpoints
+      updatedTimeline = _.merge(doc._source, data.cascadedUpdates.timeline.updated);  // eslint-disable-line no-underscore-dangle
+    }
 
-    const merged = _.assign(doc._source, { milestones }); // eslint-disable-line no-underscore-dangle
+    const merged = _.assign(updatedTimeline, { milestones });
     yield eClient.update({
       index: ES_TIMELINE_INDEX,
       type: ES_TIMELINE_TYPE,
@@ -240,14 +219,18 @@ async function milestoneUpdatedKafkaHandler(app, topic, payload) {
           }, ['progress', 'duration']);
           app.logger.debug(`Updated phase progress ${timeline.progress} and duration ${timeline.duration}`);
           app.logger.debug('Raising node event for PROJECT_PHASE_UPDATED');
-          app.emit(EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED, {
-            req: {
+          util.sendResourceToKafkaBus(
+            {
               params: { projectId: project.id, phaseId: phase.id },
               authUser: { userId: payload.userId },
             },
-            original: phase,
-            updated: _.omit(updatedPhase.toJSON(), 'deletedAt', 'deletedBy'),
-          });
+            EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
+            RESOURCES.PHASE,
+            _.omit(updatedPhase.toJSON(), 'deletedAt', 'deletedBy'),
+            phase,
+            _.get(project, 'details.settings.workstreams') ? ROUTES.WORKS.UPDATE : ROUTES.PHASES.UPDATE,
+            true, // don't send event to Notification Service as the main event here is updating milestones, not phase
+          );
         }
       }
     }
diff --git a/src/events/projectPhases/index.js b/src/events/projectPhases/index.js
index d9e8c21..e8e3337 100644
--- a/src/events/projectPhases/index.js
+++ b/src/events/projectPhases/index.js
@@ -7,6 +7,8 @@ import config from 'config';
 import _ from 'lodash';
 import Promise from 'bluebird';
 import util from '../../util';
+import { TIMELINE_REFERENCES } from '../../constants';
+
 import messageService from '../../services/messageService';
 
 const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
@@ -14,6 +16,39 @@ const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType');
 
 const eClient = util.getElasticSearchClient();
 
+/**
+ * Build topics data based on route parameter.
+ *
+ * @param  {Object} logger  logger to log along with trace id
+ * @param  {Object} phase   phase object
+ * @param  {String} route   route value can be PHASE/WORK
+ * @returns {undefined}
+ */
+const buildTopicsData = (logger, phase, route) => {
+  if (route === TIMELINE_REFERENCES.WORK) {
+    return [{
+      tag: `work#${phase.id}-details`,
+      title: `${phase.name} - Details`,
+      reference: 'project',
+      referenceId: `${phase.projectId}`,
+      body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line
+    }, {
+      tag: `work#${phase.id}-requirements`,
+      title: `${phase.name} - Requirements`,
+      reference: 'project',
+      referenceId: `${phase.projectId}`,
+      body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line
+    }];
+  }
+  return [{
+    tag: `phase#${phase.id}`,
+    title: phase.name,
+    reference: 'project',
+    referenceId: `${phase.projectId}`,
+    body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line
+  }];
+};
+
 /**
  * Indexes the project phase in the elastic search.
  *
@@ -59,26 +94,22 @@ const indexProjectPhase = Promise.coroutine(function* (logger, phase) { // eslin
 });
 
 /**
- * Creates a new phase topic in message api.
+ * Creates topics in message api
  *
  * @param  {Object} logger  logger to log along with trace id
- * @param  {Object} msg     event payload
+ * @param  {Object} phase   phase object
+ * @param  {String} route   route value can be `phase`/`work`
  * @returns {undefined}
  */
-const createPhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint-disable-line func-names
+const createTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names
   try {
-    logger.debug('Creating topic for phase with phase', phase);
-    const topic = yield messageService.createTopic({
-      reference: 'project',
-      referenceId: `${phase.projectId}`,
-      tag: `phase#${phase.id}`,
-      title: phase.name,
-      body: 'This is the beginning of your phase discussion. During execution of this phase, all related communication will be conducted here - phase updates, questions and answers, suggestions, etc. If you haven\'t already, do please take a moment to review the form in the Specification tab above and fill in as much detail as possible. This will help get started faster. Thanks!', // eslint-disable-line
-    }, logger);
-    logger.debug('topic for the phase created successfully');
-    logger.debug('created topic', topic);
+    logger.debug(`Creating topics for ${route} with phase`, phase);
+    const topicsData = buildTopicsData(logger, phase, route);
+    const topics = yield Promise.all(_.map(topicsData, topicData => messageService.createTopic(topicData, logger)));
+    logger.debug(`topics for the ${route} created successfully`);
+    logger.debug('created topics', topics);
   } catch (error) {
-    logger.error('Error in creating topic for the project phase', error);
+    logger.error(`Error in creating topic for ${route}`, error);
     // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase
     // we can create topic for a phase manually, if somehow it fails
   }
@@ -92,12 +123,14 @@ const createPhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint
  * @returns {undefined}
  */
 const projectPhaseAddedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names
-  const phase = JSON.parse(msg.content.toString());
+  const data = JSON.parse(msg.content.toString());
+  const phase = _.get(data, 'added', {});
+  const route = _.get(data, 'route', 'PHASE');
   try {
     logger.debug('calling indexProjectPhase', phase);
     yield indexProjectPhase(logger, phase, channel);
     logger.debug('calling createPhaseTopic', phase);
-    yield createPhaseTopic(logger, phase);
+    yield createTopics(logger, phase, route);
     channel.ack(msg);
   } catch (error) {
     logger.error('Error handling project.phase.added event', error);
@@ -135,31 +168,46 @@ const updateIndexProjectPhase = Promise.coroutine(function* (logger, data) { //
 });
 
 /**
- * Creates a new phase topic in message api.
+ * Update one topic
+ *
+ * @param  {Object} logger       logger to log along with trace id
+ * @param  {Object} phase        phase object
+ * @param  {Object} topicUpdate  updated topic data
+ * @returns {undefined}
+ */
+const updateOneTopic = Promise.coroutine(function* (logger, phase, topicUpdate) { // eslint-disable-line func-names
+  const topic = yield messageService.getTopicByTag(phase.projectId, topicUpdate.tag, logger);
+  logger.trace('Topic', topic);
+  const title = topicUpdate.title;
+  const titleChanged = topic && topic.title !== title;
+  logger.trace('titleChanged', titleChanged);
+  const contentPost = topic && topic.posts && topic.posts.length > 0 ? topic.posts[0] : null;
+  logger.trace('contentPost', contentPost);
+  const postId = _.get(contentPost, 'id');
+  const content = _.get(contentPost, 'body');
+  if (postId && content && titleChanged) {
+    const updatedTopic = yield messageService.updateTopic(topic.id, { title, postId, content }, logger);
+    logger.debug('topic updated successfully');
+    logger.trace('updated topic', updatedTopic);
+  }
+});
+
+/**
+ * Update topics in message api.
  *
  * @param  {Object} logger  logger to log along with trace id
- * @param  {Object} msg     event payload
+ * @param  {Object} phase   phase object
+ * @param  {String} route   route value can be `phase`/`work`
  * @returns {undefined}
  */
-const updatePhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint-disable-line func-names
+const updateTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names
   try {
-    logger.debug('Updating topic for phase with phase', phase);
-    const topic = yield messageService.getPhaseTopic(phase.projectId, phase.id, logger);
-    logger.trace('Topic', topic);
-    const title = phase.name;
-    const titleChanged = topic && topic.title !== title;
-    logger.trace('titleChanged', titleChanged);
-    const contentPost = topic && topic.posts && topic.posts.length > 0 ? topic.posts[0] : null;
-    logger.trace('contentPost', contentPost);
-    const postId = _.get(contentPost, 'id');
-    const content = _.get(contentPost, 'body');
-    if (postId && content && titleChanged) {
-      const updatedTopic = yield messageService.updateTopic(topic.id, { title, postId, content }, logger);
-      logger.debug('topic for the phase updated successfully');
-      logger.trace('updated topic', updatedTopic);
-    }
+    logger.debug(`Updating topic for ${route} with phase`, phase);
+    const topicsData = buildTopicsData(logger, phase, route);
+    yield Promise.all(_.map(topicsData, topicData => updateOneTopic(logger, phase, topicData)));
+    logger.debug(`topics for the ${route} updated successfully`);
   } catch (error) {
-    logger.error('Error in updating topic for the project phase', error);
+    logger.error(`Error in updating topic for ${route}`, error);
     // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase
     // we can create topic for a phase manually, if somehow it fails
   }
@@ -175,10 +223,11 @@ const updatePhaseTopic = Promise.coroutine(function* (logger, phase) { // eslint
 const projectPhaseUpdatedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names
   try {
     const data = JSON.parse(msg.content.toString());
+    const route = _.get(data, 'route', 'PHASE');
     logger.debug('calling updateIndexProjectPhase', data);
     yield updateIndexProjectPhase(logger, data, channel);
-    logger.debug('calling updatePhaseTopic', data.updated);
-    yield updatePhaseTopic(logger, data.updated);
+    logger.debug('calling updateTopics', data.updated);
+    yield updateTopics(logger, data.updated, route);
     channel.ack(msg);
   } catch (error) {
     logger.error('Error handling project.phase.updated event', error);
@@ -197,13 +246,14 @@ const projectPhaseUpdatedHandler = Promise.coroutine(function* (logger, msg, cha
 const removePhaseFromIndex = Promise.coroutine(function* (logger, msg) { // eslint-disable-line func-names
   try {
     const data = JSON.parse(msg.content.toString());
-    const doc = yield eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: data.projectId });
-    const phases = _.filter(doc._source.phases, single => single.id !== data.id); // eslint-disable-line no-underscore-dangle
+    const phase = _.get(data, 'deleted', {});
+    const doc = yield eClient.get({ index: ES_PROJECT_INDEX, type: ES_PROJECT_TYPE, id: phase.projectId });
+    const phases = _.filter(doc._source.phases, single => single.id !== phase.id); // eslint-disable-line no-underscore-dangle
     const merged = _.assign(doc._source, { phases });       // eslint-disable-line no-underscore-dangle
     yield eClient.update({
       index: ES_PROJECT_INDEX,
       type: ES_PROJECT_TYPE,
-      id: data.projectId,
+      id: phase.projectId,
       body: {
         doc: merged,
       },
@@ -217,26 +267,46 @@ const removePhaseFromIndex = Promise.coroutine(function* (logger, msg) { // esli
 });
 
 /**
- * Removes the phase topic from the message api.
+ * Removes one topic from the message api.
  *
- * @param  {Object} logger  logger to log along with trace id
- * @param  {Object} msg     event payload
+ * @param  {Object} logger logger to log along with trace id
+ * @param  {Object} phase  phase object
+ * @param  {Object} tag    topic tag
  * @returns {undefined}
  */
-const removePhaseTopic = Promise.coroutine(function* (logger, msg) { // eslint-disable-line func-names
+const removeOneTopic = Promise.coroutine(function* (logger, phase, tag) { // eslint-disable-line func-names
   try {
-    const phase = JSON.parse(msg.content.toString());
-    const phaseTopic = yield messageService.getPhaseTopic(phase.projectId, phase.id, logger);
+    const phaseTopic = yield messageService.getTopicByTag(phase.projectId, tag, logger);
     yield messageService.deletePosts(phaseTopic.id, phaseTopic.postIds, logger);
     yield messageService.deleteTopic(phaseTopic.id, logger);
-    logger.debug('topic for the phase removed successfully');
   } catch (error) {
-    logger.error('Error in removing topic for the project phase', error);
+    logger.error(`Error removing topic by tab ${tag}`, error);
     // don't throw the error back to nack the bus
     // we can delete topic for a phase manually, if somehow it fails
   }
 });
 
+/**
+ * Remove topics in message api.
+ *
+ * @param  {Object} logger  logger to log along with trace id
+ * @param  {Object} phase   phase object
+ * @param  {String} route   route value can be `phase`/`work`
+ * @returns {undefined}
+ */
+const removeTopics = Promise.coroutine(function* (logger, phase, route) { // eslint-disable-line func-names
+  try {
+    logger.debug(`Removing topic for ${route} with phase`, phase);
+    const topicsData = buildTopicsData(logger, phase, route);
+    yield Promise.all(_.map(topicsData, topicData => removeOneTopic(logger, phase, topicData.tag)));
+    logger.debug(`topics for the ${route} removed successfully`);
+  } catch (error) {
+    logger.error(`Error in removing topic for ${route}`, error);
+    // don't throw the error back to nack the bus, because we don't want to get multiple topics per phase
+    // we can create topic for a phase manually, if somehow it fails
+  }
+});
+
 /**
  * Handler for project phase deleted event
  * @param  {Object} logger  logger to log along with trace id
@@ -247,7 +317,11 @@ const removePhaseTopic = Promise.coroutine(function* (logger, msg) { // eslint-d
 const projectPhaseRemovedHandler = Promise.coroutine(function* (logger, msg, channel) { // eslint-disable-line func-names
   try {
     yield removePhaseFromIndex(logger, msg, channel);
-    yield removePhaseTopic(logger, msg);
+    const data = JSON.parse(msg.content.toString());
+    const phase = _.get(data, 'deleted', {});
+    const route = _.get(data, 'route');
+    logger.debug('calling removeTopics');
+    yield removeTopics(logger, phase, route);
     channel.ack(msg);
   } catch (error) {
     logger.error('Error fetching project document from elasticsearch', error);
@@ -261,5 +335,5 @@ module.exports = {
   projectPhaseAddedHandler,
   projectPhaseRemovedHandler,
   projectPhaseUpdatedHandler,
-  createPhaseTopic,
+  createPhaseTopic: createTopics,
 };
diff --git a/src/events/projects/index.spec.js b/src/events/projects/index.spec.js
index 2b149f8..3184a42 100644
--- a/src/events/projects/index.spec.js
+++ b/src/events/projects/index.spec.js
@@ -94,6 +94,7 @@ describe('projectUpdatedKafkaHandler', () => {
 
     beforeEach(async () => {
       await testUtil.clearDb();
+      await testUtil.clearES();
       project = await models.Project.create({
         type: 'generic',
         billingAccountId: 1,
diff --git a/src/models/buildingBlock.js b/src/models/buildingBlock.js
new file mode 100644
index 0000000..a28762a
--- /dev/null
+++ b/src/models/buildingBlock.js
@@ -0,0 +1,57 @@
+/* eslint-disable valid-jsdoc */
+/**
+ * BuildingBlock model
+ *
+ * WARNING: This model contains sensitive data!
+ *
+ * - To return data from this model to the user always use methods `find`/`findAll` which would
+ *   filter out the sensitive data which should be never returned to the user.
+ * - For internal usage you can use `options.includePrivateConfigForInternalUsage`
+ *   which would force `find`/`findAll` to return fields which contain sensitive data.
+ *   Use the data returned in such way ONLY FOR INTERNAL usage. It means such data can be used
+ *   to make some calculations inside Project Service but it should be never returned to the user as it is.
+ */
+module.exports = (sequelize, DataTypes) => {
+  const BuildingBlock = sequelize.define('BuildingBlock', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    key: { type: DataTypes.STRING(255), allowNull: false, unique: true },
+    config: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+    privateConfig: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+    deletedAt: DataTypes.DATE,
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    deletedBy: DataTypes.BIGINT,
+    createdBy: { type: DataTypes.BIGINT, allowNull: false },
+    updatedBy: { type: DataTypes.BIGINT, allowNull: false },
+  }, {
+    tableName: 'building_blocks',
+    paranoid: true,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    deletedAt: 'deletedAt',
+    hooks: {
+      /**
+       * Inside before hook we are evaluating if user has permission to retrieve `privateConfig` field.
+       * If no, we remove this field from the attributes list, so this field is not requested and thus
+       * not returned.
+       *
+       * @param {Object}   options  find/findAll options
+       */
+      afterFind: function removePrivateConfig(buildingBlocks, options) {
+        // ONLY FOR INTERNAL USAGE: don't use this option to return the data by API
+        if (!options.includePrivateConfigForInternalUsage) {
+          // try to remove privateConfig from result
+          buildingBlocks.map((block) => {
+            const b = block;
+            delete b.privateConfig;
+            return b;
+          });
+        }
+        return buildingBlocks;
+      },
+    },
+  });
+
+  return BuildingBlock;
+};
diff --git a/src/models/form.js b/src/models/form.js
index 1c41b53..bf7ebe3 100644
--- a/src/models/form.js
+++ b/src/models/form.js
@@ -42,6 +42,7 @@ module.exports = (sequelize, DataTypes) => {
   Form.latestVersion = classMethods.latestVersion;
   Form.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion;
   Form.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed;
+  Form.findOneWithLatestRevision = classMethods.findOneWithLatestRevision;
 
   return Form;
 };
diff --git a/src/models/milestone.js b/src/models/milestone.js
index 1f01ae7..644f7b0 100644
--- a/src/models/milestone.js
+++ b/src/models/milestone.js
@@ -1,6 +1,54 @@
+import _ from 'lodash';
 import moment from 'moment';
+import models from '../models';
+import { STATUS_HISTORY_REFERENCES } from '../constants';
 /* eslint-disable valid-jsdoc */
 
+/**
+ * Populate and map milestone model with statusHistory
+ * NOTE that this function mutates milestone
+ *
+ * @param {Array|Object} milestone one milestone or list of milestones
+ * @param {Object}       options   options which has been used to call main method
+ *
+ * @returns {Promise} promise
+ */
+const populateWithStatusHistory = async (milestone, options) => {
+  // depend on this option `milestone` is a sequlize ORM object or plain JS object
+  const isRaw = !!_.get(options, 'raw');
+  const getMilestoneId = m => (
+    isRaw ? m.id : m.dataValues.id
+  );
+  const formatMilestone = statusHistory => (
+    isRaw ? { statusHistory } : { dataValues: { statusHistory } }
+  );
+  if (Array.isArray(milestone)) {
+    const allStatusHistory = await models.StatusHistory.findAll({
+      where: {
+        referenceId: { $in: milestone.map(getMilestoneId) },
+        reference: 'milestone',
+      },
+      order: [['createdAt', 'desc']],
+      raw: true,
+    });
+
+    return milestone.map((m, index) => {
+      const statusHistory = _.filter(allStatusHistory, { referenceId: getMilestoneId(m) });
+      return _.merge(milestone[index], formatMilestone(statusHistory));
+    });
+  }
+
+  const statusHistory = await models.StatusHistory.findAll({
+    where: {
+      referenceId: getMilestoneId(milestone),
+      reference: 'milestone',
+    },
+    order: [['createdAt', 'desc']],
+    raw: true,
+  });
+  return _.merge(milestone, formatMilestone(statusHistory));
+};
+
 /**
  * The Milestone model
  */
@@ -18,10 +66,10 @@ module.exports = (sequelize, DataTypes) => {
     type: { type: DataTypes.STRING(45), allowNull: false },
     details: DataTypes.JSON,
     order: { type: DataTypes.INTEGER, allowNull: false },
-    plannedText: { type: DataTypes.STRING(512), allowNull: false },
-    activeText: { type: DataTypes.STRING(512), allowNull: false },
-    completedText: { type: DataTypes.STRING(512), allowNull: false },
-    blockedText: { type: DataTypes.STRING(512), allowNull: false },
+    plannedText: { type: DataTypes.STRING(512) },
+    activeText: { type: DataTypes.STRING(512) },
+    completedText: { type: DataTypes.STRING(512) },
+    blockedText: { type: DataTypes.STRING(512) },
     hidden: { type: DataTypes.BOOLEAN, defaultValue: false },
     deletedAt: DataTypes.DATE,
     createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
@@ -36,6 +84,54 @@ module.exports = (sequelize, DataTypes) => {
     updatedAt: 'updatedAt',
     createdAt: 'createdAt',
     deletedAt: 'deletedAt',
+    hooks: {
+      afterCreate: (milestone, options) => models.StatusHistory.create({
+        reference: STATUS_HISTORY_REFERENCES.MILESTONE,
+        referenceId: milestone.id,
+        status: milestone.status,
+        comment: null,
+        createdBy: milestone.createdBy,
+        updatedBy: milestone.updatedBy,
+      }, {
+        transaction: options.transaction,
+      }).then(() => populateWithStatusHistory(milestone, options)),
+
+      afterBulkCreate: (milestones, options) => {
+        const listStatusHistory = milestones.map(({ dataValues }) => ({
+          reference: STATUS_HISTORY_REFERENCES.MILESTONE,
+          referenceId: dataValues.id,
+          status: dataValues.status,
+          comment: null,
+          createdBy: dataValues.createdBy,
+          updatedBy: dataValues.updatedBy,
+        }));
+
+        return models.StatusHistory.bulkCreate(listStatusHistory, {
+          transaction: options.transaction,
+        }).then(() => populateWithStatusHistory(milestones, options));
+      },
+
+      afterUpdate: (milestone, options) => {
+        if (milestone.changed().includes('status')) {
+          return models.StatusHistory.create({
+            reference: STATUS_HISTORY_REFERENCES.MILESTONE,
+            referenceId: milestone.id,
+            status: milestone.status,
+            comment: options.comment || null,
+            createdBy: milestone.createdBy,
+            updatedBy: milestone.updatedBy,
+          }, {
+            transaction: options.transaction,
+          }).then(() => populateWithStatusHistory(milestone));
+        }
+        return populateWithStatusHistory(milestone, options);
+      },
+
+      afterFind: (milestone, options) => {
+        if (!milestone) return Promise.resolve();
+        return populateWithStatusHistory(milestone, options);
+      },
+    },
   });
 
   /**
diff --git a/src/models/phaseWorkStream.js b/src/models/phaseWorkStream.js
new file mode 100644
index 0000000..d420f21
--- /dev/null
+++ b/src/models/phaseWorkStream.js
@@ -0,0 +1,14 @@
+/* eslint-disable valid-jsdoc */
+
+/**
+ * The PhaseWorkStream model
+ */
+
+module.exports = (sequelize) => {
+  const PhaseWorkStream = sequelize.define('PhaseWorkStream', {},
+    {
+      tableName: 'phase_work_streams',
+    });
+
+  return PhaseWorkStream;
+};
diff --git a/src/models/planConfig.js b/src/models/planConfig.js
index 8231a68..4db619c 100644
--- a/src/models/planConfig.js
+++ b/src/models/planConfig.js
@@ -42,6 +42,7 @@ module.exports = (sequelize, DataTypes) => {
   PlanConfig.latestVersion = classMethods.latestVersion;
   PlanConfig.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion;
   PlanConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed;
+  PlanConfig.findOneWithLatestRevision = classMethods.findOneWithLatestRevision;
 
   return PlanConfig;
 };
diff --git a/src/models/priceConfig.js b/src/models/priceConfig.js
index a954344..31f4a92 100644
--- a/src/models/priceConfig.js
+++ b/src/models/priceConfig.js
@@ -41,6 +41,7 @@ module.exports = (sequelize, DataTypes) => {
   PriceConfig.latestVersion = classMethods.latestVersion;
   PriceConfig.latestRevisionOfLatestVersion = classMethods.latestRevisionOfLatestVersion;
   PriceConfig.latestVersionIncludeUsed = classMethods.latestVersionIncludeUsed;
+  PriceConfig.findOneWithLatestRevision = classMethods.findOneWithLatestRevision;
 
   return PriceConfig;
 };
diff --git a/src/models/project.js b/src/models/project.js
index b7bd964..6cfb9d0 100644
--- a/src/models/project.js
+++ b/src/models/project.js
@@ -67,6 +67,8 @@ module.exports = function defineProject(sequelize, DataTypes) {
     Project.hasMany(models.ProjectAttachment, { as: 'attachments', foreignKey: 'projectId' });
     Project.hasMany(models.ProjectPhase, { as: 'phases', foreignKey: 'projectId' });
     Project.hasMany(models.ProjectMemberInvite, { as: 'memberInvites', foreignKey: 'projectId' });
+    Project.hasMany(models.ScopeChangeRequest, { as: 'scopeChangeRequests', foreignKey: 'projectId' });
+    Project.hasMany(models.WorkStream, { as: 'workStreams', foreignKey: 'projectId' });
   };
 
   /**
diff --git a/src/models/projectEstimation.js b/src/models/projectEstimation.js
index 5bc2a7e..9ab0d8b 100644
--- a/src/models/projectEstimation.js
+++ b/src/models/projectEstimation.js
@@ -1,33 +1,29 @@
-module.exports = function defineProjectHistory(sequelize, DataTypes) {
-  const ProjectEstimation = sequelize.define(
-    'ProjectEstimation',
-    {
-      id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
-      buildingBlockKey: { type: DataTypes.STRING, allowNull: false },
-      conditions: { type: DataTypes.STRING, allowNull: false },
-      price: { type: DataTypes.DOUBLE, allowNull: false },
-      quantity: { type: DataTypes.INTEGER, allowNull: true },
-      minTime: { type: DataTypes.INTEGER, allowNull: false },
-      maxTime: { type: DataTypes.INTEGER, allowNull: false },
-      metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
-      projectId: { type: DataTypes.BIGINT, allowNull: false },
+module.exports = function defineProjectEstimation(sequelize, DataTypes) {
+  const ProjectEstimation = sequelize.define('ProjectEstimation', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    buildingBlockKey: { type: DataTypes.STRING, allowNull: false },
+    conditions: { type: DataTypes.STRING, allowNull: false },
+    price: { type: DataTypes.DOUBLE, allowNull: false },
+    quantity: { type: DataTypes.INTEGER, allowNull: true },
+    minTime: { type: DataTypes.INTEGER, allowNull: false },
+    maxTime: { type: DataTypes.INTEGER, allowNull: false },
+    metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+    projectId: { type: DataTypes.BIGINT, allowNull: false },
 
-      deletedAt: { type: DataTypes.DATE, allowNull: true },
-      createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
-      updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
-      deletedBy: DataTypes.BIGINT,
-      createdBy: { type: DataTypes.INTEGER, allowNull: false },
-      updatedBy: { type: DataTypes.INTEGER, allowNull: false },
-    },
-    {
-      tableName: 'project_estimations',
-      paranoid: true,
-      timestamps: true,
-      updatedAt: 'updatedAt',
-      createdAt: 'createdAt',
-      indexes: [],
-    },
-  );
+    deletedAt: { type: DataTypes.DATE, allowNull: true },
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    deletedBy: DataTypes.BIGINT,
+    createdBy: { type: DataTypes.INTEGER, allowNull: false },
+    updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+  }, {
+    tableName: 'project_estimations',
+    paranoid: true,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    indexes: [],
+  });
 
   return ProjectEstimation;
 };
diff --git a/src/models/projectEstimationItem.js b/src/models/projectEstimationItem.js
new file mode 100644
index 0000000..0e0aeb8
--- /dev/null
+++ b/src/models/projectEstimationItem.js
@@ -0,0 +1,164 @@
+/* eslint-disable valid-jsdoc */
+/**
+ * ProjectEstimationItem model
+ *
+ * WARNING: This model contains sensitive data!
+ *
+ * - To return data from this model to the user always use methods `find`/`findAll`
+ *   and provide to them `options.reqUser` and `options.members` to check what
+ *   types of Project Estimation Items user which makes the request can get.
+ * - For internal usage you can use `options.includeAllProjectEstimatinoItemsForInternalUsage`
+ *   which would force `find`/`findAll` to return all the records without checking permissions.
+ *   Use the data returned in such way ONLY FOR INTERNAL usage. It means such data can be used
+ *   to make some calculations inside Project Service but it should be never returned to the user as it is.
+ */
+import _ from 'lodash';
+import util from '../util';
+import {
+  ESTIMATION_TYPE,
+  MANAGER_ROLES,
+  PROJECT_MEMBER_ROLE,
+} from '../constants';
+
+/*
+  This config defines which Project Estimation Item `types` users can get
+  based on their permissions
+ */
+const permissionsConfigs = [
+  // Topcoder managers can get all types of Project Estimation Items
+  {
+    permission: { topcoderRoles: MANAGER_ROLES },
+    types: _.values(ESTIMATION_TYPE),
+  },
+
+  // Project Copilots can get only 'community' type of Project Estimation Items
+  {
+    permission: { projectRoles: PROJECT_MEMBER_ROLE.COPILOT },
+    types: [ESTIMATION_TYPE.COMMUNITY],
+  },
+];
+
+module.exports = function defineProjectEstimationItem(sequelize, DataTypes) {
+  const ProjectEstimationItem = sequelize.define(
+    'ProjectEstimationItem',
+    {
+      id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+      projectEstimationId: { type: DataTypes.BIGINT, allowNull: false }, // ProjectEstimation id
+      price: { type: DataTypes.DOUBLE, allowNull: false },
+      type: {
+        type: DataTypes.STRING,
+        allowNull: false,
+        validate: {
+          isIn: [_.values(ESTIMATION_TYPE)],
+        },
+      },
+      markupUsedReference: { type: DataTypes.STRING, allowNull: false },
+      markupUsedReferenceId: { type: DataTypes.BIGINT, allowNull: false }, // ProjectSetting id
+      metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+      deletedAt: { type: DataTypes.DATE, allowNull: true },
+      createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+      updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+      deletedBy: DataTypes.INTEGER,
+      createdBy: { type: DataTypes.INTEGER, allowNull: false },
+      updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+    },
+    {
+      tableName: 'project_estimation_items',
+      paranoid: true,
+      timestamps: true,
+      updatedAt: 'updatedAt',
+      createdAt: 'createdAt',
+      indexes: [],
+      hooks: {
+        /**
+         * Inside before hook we are evaluating what Project Estimation Item types current user may retrieve.
+         * We update `where` query so only allowed types may be retrieved.
+         *
+         * @param {Object}   options  find/findAll options
+         * @param {Function} callback callback after hook
+         */
+        beforeFind: (options, callback) => {
+          // ONLY FOR INTERNAL USAGE: don't use this option to return the data by API
+          if (options.includeAllProjectEstimatinoItemsForInternalUsage) {
+            return callback ? callback(null) : null;
+          }
+
+          if (!options.reqUser || !options.members) {
+            const err = new Error('You must provide auth user and project members to get project estimation items');
+            if (!callback) throw err;
+            return callback(err);
+          }
+
+          // find all project estimation item types which are allowed to be returned to the user
+          let allowedTypes = [];
+          permissionsConfigs.forEach((permissionsConfig) => {
+            if (util.hasPermission(permissionsConfig.permission, options.reqUser, options.members)) {
+              allowedTypes = _.concat(allowedTypes, permissionsConfig.types);
+            }
+          });
+          allowedTypes = _.uniq(allowedTypes);
+
+          // only return Project Estimation Types which are allowed to the user
+          options.where.type = allowedTypes; // eslint-disable-line no-param-reassign
+          return callback ? callback(null) : null;
+        },
+      },
+    },
+  );
+
+  /**
+   * Find all project estimation items for project
+   *
+   * TODO: this method can rewritten without using `models`
+   *       and using JOIN instead for retrieving ProjectEstimationTimes by projectId
+   *
+   * @param {Object} models    all models
+   * @param {Number} projectId project id
+   * @param {Object} [options] options
+   *
+   * @returns {Promise} list of project estimation items
+   */
+  ProjectEstimationItem.findAllByProject = (models, projectId, options) =>
+    models.ProjectEstimation.findAll({
+      raw: true,
+      where: {
+        projectId,
+      },
+    }).then((estimations) => {
+      const optionsCombined = _.assign({}, options);
+      // update where to always filter by projectEstimationsIds of the project
+      optionsCombined.where = _.assign({}, optionsCombined.where, {
+        projectEstimationId: _.map(estimations, 'id'),
+      });
+
+      return ProjectEstimationItem.findAll(optionsCombined);
+    });
+
+  /**
+   * Delete all project estimation items for project
+   *
+   * TODO: this method can rewritten without using `models`
+   *       and using JOIN instead for retrieving ProjectEstimationTimes by projectId
+   *
+   * @param {Object} models    all models
+   * @param {Number} projectId project id
+   * @param {Object} reqUser   user who makes the request
+   * @param {Object} [options] options
+   *
+   * @returns {Promise} result of destroy query
+   */
+  ProjectEstimationItem.deleteAllForProject = (models, projectId, reqUser, options) =>
+    ProjectEstimationItem.findAllByProject(models, projectId, options)
+      .then((estimationItems) => {
+        const estimationItemsOptions = {
+          where: {
+            id: _.map(estimationItems, 'id'),
+          },
+        };
+
+        return ProjectEstimationItem.update({ deletedBy: reqUser.userId }, estimationItemsOptions)
+          .then(() => ProjectEstimationItem.destroy(estimationItemsOptions));
+      });
+
+  return ProjectEstimationItem;
+};
diff --git a/src/models/projectMemberInvite.js b/src/models/projectMemberInvite.js
index f45d4b7..bacee6c 100644
--- a/src/models/projectMemberInvite.js
+++ b/src/models/projectMemberInvite.js
@@ -67,7 +67,10 @@ module.exports = function defineProjectMemberInvite(sequelize, DataTypes) {
     const where = { projectId, status: INVITE_STATUS.PENDING };
 
     if (email && userId) {
-      _.assign(where, { $or: [{ email: { $eq: email } }, { userId: { $eq: userId } }] });
+      _.assign(where, { $or: [
+        { email: { $eq: email.toLowerCase() } },
+        { userId: { $eq: userId } },
+      ] });
     } else if (email) {
       _.assign(where, { email });
     } else if (userId) {
diff --git a/src/models/projectPhase.js b/src/models/projectPhase.js
index a74675c..5c260e1 100644
--- a/src/models/projectPhase.js
+++ b/src/models/projectPhase.js
@@ -4,6 +4,8 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) {
   const ProjectPhase = sequelize.define('ProjectPhase', {
     id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
     name: { type: DataTypes.STRING, allowNull: true },
+    description: { type: DataTypes.STRING, allowNull: true },
+    requirements: { type: DataTypes.STRING, allowNull: true },
     status: { type: DataTypes.STRING, allowNull: true },
     startDate: { type: DataTypes.DATE, allowNull: true },
     endDate: { type: DataTypes.DATE, allowNull: true },
@@ -40,6 +42,7 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) {
 
   ProjectPhase.associate = (models) => {
     ProjectPhase.hasMany(models.PhaseProduct, { as: 'products', foreignKey: 'phaseId' });
+    ProjectPhase.belongsToMany(models.WorkStream, { through: models.PhaseWorkStream, foreignKey: 'phaseId' });
   };
 
   /**
@@ -53,22 +56,28 @@ module.exports = function defineProjectPhase(sequelize, DataTypes) {
    * @return {Object} the result rows and count
    */
   ProjectPhase.search = async (parameters = {}, log) => {
-    let fieldsStr = _.map(parameters.fields, field => `project_phases."${field}"`);
-    fieldsStr = `${fieldsStr.join(',')}`;
-    const replacements = {
-      projectId: parameters.projectId,
-    };
-    let dbQuery = `SELECT ${fieldsStr} FROM project_phases WHERE project_phases."projectId" = :projectId`;
+    // ordering
+    const orderBy = [];
     if (_.has(parameters, 'sortField') && _.has(parameters, 'sortType')) {
-      dbQuery = `${dbQuery} ORDER BY project_phases."${parameters.sortField}" ${parameters.sortType}`;
+      orderBy.push([parameters.sortField, parameters.sortType]);
+    }
+    // find options
+    const options = {
+      where: {
+        projectId: parameters.projectId,
+      },
+      order: orderBy,
+      logging: (str) => { log.debug(str); },
+    };
+    // select fields
+    if (_.has(parameters, 'fields')) {
+      _.set(options, 'attributes', parameters.fields.filter(e => e !== 'products'));
+      if (parameters.fields.includes('products')) {
+        _.set(options, 'include', [{ model: this.sequelize.models.PhaseProduct, as: 'products' }]);
+      }
     }
-    return sequelize.query(dbQuery,
-      { type: sequelize.QueryTypes.SELECT,
-        logging: (str) => { log.debug(str); },
-        replacements,
-        raw: true,
-      })
-      .then(phases => ({ rows: phases, count: phases.length }));
+
+    return ProjectPhase.findAll(options).then(phases => ({ rows: phases, count: phases.length }));
   };
 
   return ProjectPhase;
diff --git a/src/models/projectSetting.js b/src/models/projectSetting.js
new file mode 100644
index 0000000..dd72026
--- /dev/null
+++ b/src/models/projectSetting.js
@@ -0,0 +1,115 @@
+/* eslint-disable valid-jsdoc */
+/**
+ * ProjectSetting model
+ *
+ * WARNING: This model contains sensitive data!
+ *
+ * - To return data from this model to the user always use methods `find`/`findAll`
+ *   and provide to them `options.reqUser` and `options.members` to check which records could be returned
+ *   based on the user roles and `readPermission` property of the records.
+ * - For internal usage you can use `options.includeAllProjectSettingsForInternalUsage`
+ *   which would force `find`/`findAll` to return all the records without checking permissions.
+ *   Use the data returned in such way ONLY FOR INTERNAL usage. It means such data can be used
+ *   to make some calculations inside Project Service but it should be never returned to the user as it is.
+ */
+
+import _ from 'lodash';
+import { VALUE_TYPE } from '../constants';
+import util from '../util';
+
+module.exports = (sequelize, DataTypes) => {
+  const ProjectSetting = sequelize.define(
+    'ProjectSetting',
+    {
+      id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+      key: { type: DataTypes.STRING(255) },
+      value: { type: DataTypes.STRING(255) },
+      valueType: {
+        type: DataTypes.STRING,
+        validate: {
+          isIn: [_.values(VALUE_TYPE)],
+        },
+      },
+      projectId: { type: DataTypes.BIGINT, allowNull: false }, // Project id
+      metadata: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+      readPermission: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+      writePermission: { type: DataTypes.JSON, allowNull: false, defaultValue: {} },
+      deletedAt: { type: DataTypes.DATE, allowNull: true },
+      createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+      updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+      deletedBy: DataTypes.INTEGER,
+      createdBy: { type: DataTypes.INTEGER, allowNull: false },
+      updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+    },
+    {
+      tableName: 'project_settings',
+      paranoid: true,
+      timestamps: true,
+      updatedAt: 'updatedAt',
+      createdAt: 'createdAt',
+      indexes: [
+        {
+          unique: true,
+          fields: ['key', 'projectId'],
+        },
+      ],
+      hooks: {
+        /**
+         * Inside before hook we are checking that required options are provided
+         * We do it in `beforeFind` instead of `afterFind` to avoid unnecessary data retrievement
+         *
+         * @param {Object}   options  find/findAll options
+         * @param {Function} callback callback after hook
+         */
+        beforeFind: (options, callback) => {
+          // ONLY FOR INTERNAL USAGE: don't use this option to return the data by API
+          if (options.includeAllProjectSettingsForInternalUsage) {
+            return callback ? callback(null) : null;
+          }
+
+          if (!options.reqUser || !options.members) {
+            const err = new Error('You must provide reqUser and project member to get project settings');
+            if (!callback) throw err;
+            return callback(err);
+          }
+
+          return callback ? callback(null) : null;
+        },
+
+        /**
+         * Inside after hook we are filtering records based on `readPermission` and user roles
+         *
+         * @param {Mixed}    results  one result from `find()` or array of results form `findAll()`
+         * @param {Object}   options  find/findAll options
+         * @param {Function} callback callback after hook
+         */
+        afterFind: (results, options, callback) => {
+          // ONLY FOR INTERNAL USAGE: don't use this option to return the data by API
+          if (options.includeAllProjectSettingsForInternalUsage) {
+            return callback ? callback(null) : null;
+          }
+
+          // if we have an array of results form `findAll()` we are filtering results
+          if (_.isArray(results)) {
+            // remove results from the "end" using `index` if user doesn't have permissions for to access them
+            for (let index = results.length - 1; index >= 0; index -= 1) {
+              if (!util.hasPermission(results[index].readPermission, options.reqUser, options.members)) {
+                results.splice(index, 1);
+              }
+            }
+
+          // if we have one result from `find()` we check if user has permission for the record
+          } else if (results && !util.hasPermission(results.readPermission, options.reqUser, options.members)) {
+            const err = new Error('User doesn\'t have permission to access this record.');
+            if (!callback) throw err;
+            return callback(err);
+          }
+
+          return callback ? callback(null) : null;
+        },
+      },
+    },
+  );
+
+  return ProjectSetting;
+};
diff --git a/src/models/projectTemplate.js b/src/models/projectTemplate.js
index dd61765..2d14299 100644
--- a/src/models/projectTemplate.js
+++ b/src/models/projectTemplate.js
@@ -1,4 +1,7 @@
 /* eslint-disable valid-jsdoc */
+import _ from 'lodash';
+
+import models from './';
 
 /**
  * The Project Template model
@@ -35,5 +38,15 @@ module.exports = (sequelize, DataTypes) => {
     deletedAt: 'deletedAt',
   });
 
+  ProjectTemplate.getTemplate = templateId =>
+    ProjectTemplate.findByPk(templateId, { raw: true })
+      .then((template) => {
+        const formRef = template.form;
+        return formRef
+          ? models.Form.findAll({ where: formRef, raw: true })
+            .then(forms => Object.assign({}, template, { form: _.maxBy(forms, f => f.revision) }))
+          : template;
+      });
+
   return ProjectTemplate;
 };
diff --git a/src/models/scopeChangeRequest.js b/src/models/scopeChangeRequest.js
new file mode 100644
index 0000000..e527b0c
--- /dev/null
+++ b/src/models/scopeChangeRequest.js
@@ -0,0 +1,68 @@
+/* eslint-disable valid-jsdoc */
+import { SCOPE_CHANGE_REQ_STATUS } from '../constants';
+
+/**
+ * The ScopeChangeRequest model
+ */
+
+module.exports = (sequelize, DataTypes) => {
+  const ScopeChangeRequest = sequelize.define('ScopeChangeRequest', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    projectId: { type: DataTypes.BIGINT, allowNull: false },
+    oldScope: { type: DataTypes.JSON, allowNull: false },
+    newScope: { type: DataTypes.JSON, allowNull: false },
+    status: { type: DataTypes.STRING(45), allowNull: false },
+
+    deletedAt: { type: DataTypes.DATE, allowNull: true },
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    approvedAt: { type: DataTypes.DATE, allowNull: true },
+    deletedBy: { type: DataTypes.INTEGER, allowNull: true },
+    createdBy: { type: DataTypes.INTEGER, allowNull: false },
+    updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+    approvedBy: { type: DataTypes.INTEGER, allowNull: true },
+  }, {
+    tableName: 'scope_change_requests',
+    paranoid: true,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    deletedAt: 'deletedAt',
+  });
+
+  ScopeChangeRequest.findScopeChangeRequest = (projectId, { requestId, status }) => {
+    const where = {
+      projectId,
+    };
+    if (status) {
+      where.status = status;
+    }
+    if (requestId) {
+      where.id = requestId;
+    }
+    return ScopeChangeRequest.findOne({
+      where,
+    });
+  };
+
+  ScopeChangeRequest.findPendingScopeChangeRequest = projectId =>
+    ScopeChangeRequest.findScopeChangeRequest(
+      projectId,
+      { status: { $in: [SCOPE_CHANGE_REQ_STATUS.PENDING, SCOPE_CHANGE_REQ_STATUS.APPROVED] } },
+    );
+
+  ScopeChangeRequest.getProjectScopeChangeRequests = (projectId, status) => {
+    const where = {
+      projectId,
+    };
+    if (status) {
+      where.status = status;
+    }
+    return ScopeChangeRequest.findAll({
+      where,
+      raw: true,
+    });
+  };
+
+  return ScopeChangeRequest;
+};
diff --git a/src/models/statusHistory.js b/src/models/statusHistory.js
new file mode 100644
index 0000000..b73d365
--- /dev/null
+++ b/src/models/statusHistory.js
@@ -0,0 +1,35 @@
+/* eslint-disable valid-jsdoc */
+
+import _ from 'lodash';
+import { MILESTONE_STATUS } from '../constants';
+
+module.exports = function defineStatusHistory(sequelize, DataTypes) {
+  const StatusHistory = sequelize.define('StatusHistory', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    reference: { type: DataTypes.STRING, allowNull: false },
+    referenceId: { type: DataTypes.BIGINT, allowNull: false },
+    status: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      validate: {
+        isIn: [_.values(MILESTONE_STATUS)],
+      },
+    },
+    comment: DataTypes.TEXT,
+    createdBy: { type: DataTypes.INTEGER, allowNull: false },
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+  }, {
+    tableName: 'status_history',
+    paranoid: false,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    deletedAt: 'deletedAt',
+    indexes: [],
+    classMethods: {},
+  });
+
+  return StatusHistory;
+};
diff --git a/src/models/versionModelClassMethods.js b/src/models/versionModelClassMethods.js
index 13d79d7..bcf7c7f 100644
--- a/src/models/versionModelClassMethods.js
+++ b/src/models/versionModelClassMethods.js
@@ -9,6 +9,17 @@
  */
 function versionModelClassMethods(model, jsonField) {
   return {
+    findOneWithLatestRevision(query) {
+      return model.findOne({
+        where: {
+          key: query.key,
+          version: query.version,
+        },
+        order: [['revision', 'DESC']],
+        limit: 1,
+        attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      });
+    },
     deleteOldestRevision(userId, key, version) {
       return model.findOne({
         where: {
diff --git a/src/models/workManagementPermission.js b/src/models/workManagementPermission.js
new file mode 100644
index 0000000..17915e6
--- /dev/null
+++ b/src/models/workManagementPermission.js
@@ -0,0 +1,35 @@
+/* eslint-disable valid-jsdoc */
+
+/**
+ * The WorkManagementPermission model
+ */
+module.exports = (sequelize, DataTypes) => {
+  const WorkManagementPermission = sequelize.define('WorkManagementPermission', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    policy: { type: DataTypes.STRING(255), allowNull: false },
+    permission: { type: DataTypes.JSON, allowNull: false },
+    projectTemplateId: { type: DataTypes.BIGINT, allowNull: false },
+
+    deletedAt: { type: DataTypes.DATE, allowNull: true },
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    deletedBy: { type: DataTypes.INTEGER, allowNull: true },
+    createdBy: { type: DataTypes.INTEGER, allowNull: false },
+    updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+  }, {
+    tableName: 'work_management_permissions',
+    paranoid: true,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    deletedAt: 'deletedAt',
+    indexes: [
+      {
+        unique: true,
+        fields: ['policy', 'projectTemplateId'],
+      },
+    ],
+  });
+
+  return WorkManagementPermission;
+};
diff --git a/src/models/workStream.js b/src/models/workStream.js
new file mode 100644
index 0000000..d43628b
--- /dev/null
+++ b/src/models/workStream.js
@@ -0,0 +1,42 @@
+/* eslint-disable valid-jsdoc */
+
+/**
+ * The WorkStream model
+ */
+import _ from 'lodash';
+import { WORKSTREAM_STATUS } from '../constants';
+
+module.exports = (sequelize, DataTypes) => {
+  const WorkStream = sequelize.define('WorkStream', {
+    id: { type: DataTypes.BIGINT, primaryKey: true, autoIncrement: true },
+    name: { type: DataTypes.STRING(255), allowNull: false },
+    type: { type: DataTypes.STRING(45), allowNull: false },
+    status: {
+      type: DataTypes.STRING,
+      allowNull: false,
+      validate: {
+        isIn: [_.values(WORKSTREAM_STATUS)],
+      },
+    },
+    deletedAt: { type: DataTypes.DATE, allowNull: true },
+    createdAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    updatedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
+    deletedBy: { type: DataTypes.INTEGER, allowNull: true },
+    createdBy: { type: DataTypes.INTEGER, allowNull: false },
+    updatedBy: { type: DataTypes.INTEGER, allowNull: false },
+  }, {
+    tableName: 'work_streams',
+    paranoid: true,
+    timestamps: true,
+    updatedAt: 'updatedAt',
+    createdAt: 'createdAt',
+    deletedAt: 'deletedAt',
+    indexes: [],
+  });
+
+  WorkStream.associate = (models) => {
+    WorkStream.belongsToMany(models.ProjectPhase, { through: models.PhaseWorkStream, foreignKey: 'workStreamId' });
+  };
+
+  return WorkStream;
+};
diff --git a/src/permissions/constants.js b/src/permissions/constants.js
new file mode 100644
index 0000000..570bc15
--- /dev/null
+++ b/src/permissions/constants.js
@@ -0,0 +1,39 @@
+/**
+ * Definitions of permissions which could be used with util methods
+ * `util.hasPermission` or `util.hasPermissionForProject`.
+ *
+ * We can define permission using two logics:
+ * 1. **WHAT** can be done with such a permission. Such constants may have names like:
+ *    - `VIEW_PROJECT`
+ *    - `EDIT_MILESTONE`
+ *    - `DELETE_WORK`
+ *    and os on.
+ * 2. **WHO** can do actions with such a permission. Such constants **MUST** start from the prefix `ROLES_`, examples:
+ *    - `ROLES_COPILOT_AND_ABOVE`
+ *    - `ROLES_PROJECT_MEMBERS`
+ *    - `ROLES_ADMINS`
+ */
+import {
+  PROJECT_MEMBER_ROLE,
+  ADMIN_ROLES,
+} from '../constants';
+
+export const PERMISSION = { // eslint-disable-line import/prefer-default-export
+  /**
+   * Permissions defined by logic: **WHO** can do actions with such a permission.
+   */
+  ROLES_COPILOT_AND_ABOVE: {
+    topcoderRoles: ADMIN_ROLES,
+    projectRoles: [
+      PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+      PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+      PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+      PROJECT_MEMBER_ROLE.MANAGER,
+      PROJECT_MEMBER_ROLE.COPILOT,
+    ],
+  },
+  /**
+   * Permissions defined by logic: **WHAT** can be done with such a permission.
+   */
+};
+
diff --git a/src/permissions/copilotAndAbove.js b/src/permissions/copilotAndAbove.js
index e5d5121..d6e8b21 100644
--- a/src/permissions/copilotAndAbove.js
+++ b/src/permissions/copilotAndAbove.js
@@ -1,18 +1,31 @@
+import _ from 'lodash';
 import util from '../util';
-import { MANAGER_ROLES, USER_ROLE } from '../constants';
-
+import models from '../models';
+import { PERMISSION } from './constants';
 
 /**
- * Permission to alloow copilot and above roles to perform certain operations
+ * Permission to allow copilot and above roles to perform certain operations
+ * - User with Topcoder admins roles should be able to perform the operations.
+ * - Project members with copilot and manager Project roles should be also able to perform the operations.
  * @param {Object}    req         the express request instance
  * @return {Promise}              returns a promise
  */
 module.exports = req => new Promise((resolve, reject) => {
-  const hasAccess = util.hasRoles(req, [...MANAGER_ROLES, USER_ROLE.COPILOT]);
+  const projectId = _.parseInt(req.params.projectId);
+
+  return models.ProjectMember.getActiveProjectMembers(projectId)
+    .then((members) => {
+      const hasPermission = util.hasPermission(PERMISSION.ROLES_COPILOT_AND_ABOVE, req.authUser, members);
 
-  if (!hasAccess) {
-    return reject(new Error('You do not have permissions to perform this action'));
-  }
+      // TODO should we really do this?
+      // if no, we can replace `getActiveProjectMembers + util.hasPermission` with one `util.hasPermissionForProject`
+      req.context = req.context || {};
+      req.context.currentProjectMembers = members;
 
-  return resolve(true);
+      if (!hasPermission) {
+        // the copilot or manager is not a registered project member
+        return reject(new Error('You do not have permissions to perform this action'));
+      }
+      return resolve(true);
+    });
 });
diff --git a/src/permissions/index.js b/src/permissions/index.js
index 431816c..33dd62a 100644
--- a/src/permissions/index.js
+++ b/src/permissions/index.js
@@ -8,8 +8,10 @@ const projectMemberDelete = require('./projectMember.delete');
 const projectAdmin = require('./admin.ops');
 const projectAttachmentUpdate = require('./project.updateAttachment');
 const projectAttachmentDownload = require('./project.downloadAttachment');
-// const connectManagerOrAdmin = require('./connectManagerOrAdmin.ops');
+const connectManagerOrAdmin = require('./connectManagerOrAdmin.ops');
 const copilotAndAbove = require('./copilotAndAbove');
+const workManagementPermissions = require('./workManagementForTemplate');
+const projectSettingEdit = require('./projectSetting.edit');
 
 module.exports = () => {
   Authorizer.setDeniedStatusCode(403);
@@ -100,4 +102,40 @@ module.exports = () => {
   Authorizer.setPolicy('planConfig.edit', projectAdmin);
   Authorizer.setPolicy('planConfig.delete', projectAdmin);
   Authorizer.setPolicy('planConfig.view', true); // anyone can view price config
+
+  // Work stream
+  Authorizer.setPolicy('workStream.create', projectAdmin);
+  Authorizer.setPolicy('workStream.edit', workManagementPermissions('workStream.edit'));
+  Authorizer.setPolicy('workStream.delete', projectAdmin);
+  Authorizer.setPolicy('workStream.view', projectView);
+
+  // Work
+  Authorizer.setPolicy('work.create', workManagementPermissions('work.create'));
+  Authorizer.setPolicy('work.edit', workManagementPermissions('work.edit'));
+  Authorizer.setPolicy('work.delete', workManagementPermissions('work.delete'));
+  Authorizer.setPolicy('work.view', projectView);
+
+  // Work item
+  Authorizer.setPolicy('workItem.create', workManagementPermissions('workItem.create'));
+  Authorizer.setPolicy('workItem.edit', workManagementPermissions('workItem.edit'));
+  Authorizer.setPolicy('workItem.delete', workManagementPermissions('workItem.delete'));
+  Authorizer.setPolicy('workItem.view', projectView);
+
+  // Work management permission
+  Authorizer.setPolicy('workManagementPermission.create', projectAdmin);
+  Authorizer.setPolicy('workManagementPermission.edit', projectAdmin);
+  Authorizer.setPolicy('workManagementPermission.delete', projectAdmin);
+  Authorizer.setPolicy('workManagementPermission.view', projectAdmin);
+
+  // Project Permissions
+  Authorizer.setPolicy('permissions.view', projectView);
+
+  // Project Settings
+  Authorizer.setPolicy('projectSetting.create', connectManagerOrAdmin);
+  Authorizer.setPolicy('projectSetting.edit', projectSettingEdit);
+  Authorizer.setPolicy('projectSetting.delete', connectManagerOrAdmin);
+  Authorizer.setPolicy('projectSetting.view', projectView);
+
+  // Project Estimation Items
+  Authorizer.setPolicy('projectEstimation.item.list', copilotAndAbove);
 };
diff --git a/src/permissions/project.delete.js b/src/permissions/project.delete.js
index 9fe49fc..1d0841c 100644
--- a/src/permissions/project.delete.js
+++ b/src/permissions/project.delete.js
@@ -23,7 +23,13 @@ module.exports = freq => new Promise((resolve, reject) => {
         const hasAccess = util.hasAdminRole(req) ||
           !_.isUndefined(_.find(members, m => m.userId === req.authUser.userId &&
             ((m.role === PROJECT_MEMBER_ROLE.CUSTOMER && m.isPrimary) ||
-              m.role === PROJECT_MEMBER_ROLE.MANAGER)));
+                [
+                  PROJECT_MEMBER_ROLE.MANAGER,
+                  PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+                  PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+                  PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+                ].includes(m.role)
+            )));
 
         if (!hasAccess) {
           // user is not an admin nor is a registered project member
diff --git a/src/permissions/projectMember.delete.js b/src/permissions/projectMember.delete.js
index 0f0ec42..5f4bb94 100644
--- a/src/permissions/projectMember.delete.js
+++ b/src/permissions/projectMember.delete.js
@@ -25,7 +25,12 @@ module.exports = freq => new Promise((resolve, reject) => {
       const memberToBeRemoved = _.find(members, m => m.id === prjMemberId);
       // check if auth user has acecss to this project
       const hasAccess = util.hasAdminRole(req)
-        || (authMember && memberToBeRemoved && (authMember.role === PROJECT_MEMBER_ROLE.MANAGER ||
+        || (authMember && memberToBeRemoved && ([
+          PROJECT_MEMBER_ROLE.MANAGER,
+          PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+          PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+          PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+        ].includes(authMember.role) ||
           (authMember.role === PROJECT_MEMBER_ROLE.CUSTOMER && authMember.isPrimary &&
             memberToBeRemoved.role === PROJECT_MEMBER_ROLE.CUSTOMER) ||
           memberToBeRemoved.userId === req.authUser.userId));
diff --git a/src/permissions/projectSetting.edit.js b/src/permissions/projectSetting.edit.js
new file mode 100644
index 0000000..0d9794c
--- /dev/null
+++ b/src/permissions/projectSetting.edit.js
@@ -0,0 +1,40 @@
+import util from '../util';
+import models from '../models';
+
+/**
+ * Only users who have "writePermission" of ProjectSetting can edit this entity.
+ * @param {Object}    freq        the express request instance
+ * @return {Promise}              Returns a promise
+ */
+module.exports = freq => new Promise((resolve, reject) => {
+  const projectId = freq.params.projectId;
+  const settingId = freq.params.id;
+  let projectMembers = [];
+
+  return models.ProjectMember.getActiveProjectMembers(projectId)
+    .then((members) => {
+      const req = freq;
+      req.context = req.context || {};
+      req.context.currentProjectMembers = members;
+
+      projectMembers = members;
+      return Promise.resolve();
+    })
+    .then(() => models.ProjectSetting.findOne({
+      where: { projectId, id: settingId },
+      raw: true,
+      includeAllProjectSettingsForInternalUsage: true,
+    }).then((setting) => {
+      if (!setting) {
+        // let route handle this 404 error.
+        return resolve(true);
+      }
+      const hasAccess = util.hasPermission(setting.writePermission, freq.authUser, projectMembers);
+      if (!hasAccess) {
+        const errorMessage = 'You do not have permissions to perform this action';
+        // user is not an admin nor is a registered project member
+        return reject(new Error(errorMessage));
+      }
+      return resolve(true);
+    }));
+});
diff --git a/src/permissions/workManagementForTemplate.js b/src/permissions/workManagementForTemplate.js
new file mode 100644
index 0000000..12c97c3
--- /dev/null
+++ b/src/permissions/workManagementForTemplate.js
@@ -0,0 +1,56 @@
+
+import _ from 'lodash';
+import util from '../util';
+import models from '../models';
+import { MANAGER_ROLES } from '../constants';
+
+/**
+ * Based on allowRule and denyRule allow/deny users to execute the policy
+ * @param {String}    policy      the work management permission policy
+ * @return {Promise}              Returns a promise
+ */
+module.exports = policy => req => new Promise((resolve, reject) => {
+  const projectId = _.parseInt(req.params.projectId);
+
+  return models.Project.findOne({
+    where: {
+      id: projectId,
+    },
+  })
+    .then((project) => {
+      if (!project) {
+        const apiErr = new Error(`Project not found for id ${projectId}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+
+      if (!project.templateId) {
+        return null;
+      }
+
+      return models.WorkManagementPermission.findOne({
+        where: {
+          policy,
+          projectTemplateId: project.templateId,
+        },
+      });
+    })
+    .then((workManagementPermission) => {
+      if (!workManagementPermission) {
+        // TODO REMOVE THIS!!!
+        // TEMPORARY let all the Topcoder managers to do all the work management
+        // if there are no permission records in the DB for the template
+        return util.hasPermission({ topcoderRoles: MANAGER_ROLES }, req.authUser);
+        // return false;
+      }
+
+      return util.hasPermissionForProject(workManagementPermission.permission, req.authUser, projectId);
+    })
+    .then((hasAccess) => {
+      if (!hasAccess) {
+        const errorMessage = 'You do not have permissions to perform this action';
+        return reject(new Error(errorMessage));
+      }
+      return resolve(true);
+    });
+});
diff --git a/src/routes/admin/project-index-create.js b/src/routes/admin/project-index-create.js
index 9a42151..a6f8dfb 100644
--- a/src/routes/admin/project-index-create.js
+++ b/src/routes/admin/project-index-create.js
@@ -117,6 +117,7 @@ module.exports = [
         })
         .then((result) => {
           logger.debug(`project indexed successfully (projectId: ${projectIdStart}-${projectIdEnd})`, result);
+          logger.debug(result);
         })
         .catch((error) => {
           logger.error(`Error in indexing project (projectId: ${projectIdStart}-${projectIdEnd})`, error);
diff --git a/src/routes/attachments/create.js b/src/routes/attachments/create.js
index c594713..f46a4a9 100644
--- a/src/routes/attachments/create.js
+++ b/src/routes/attachments/create.js
@@ -109,7 +109,7 @@ module.exports = [
     }).then((_newAttachment) => {
       newAttachment = _newAttachment.get({ plain: true });
       req.log.debug('New Attachment record: ', newAttachment);
-      if (process.env.NODE_ENV !== 'development') {
+      if (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') {
         // retrieve download url for the response
         req.log.debug('retrieving download url');
         return httpClient.post(`${fileServiceUrl}downloadurl`, {
@@ -118,7 +118,7 @@ module.exports = [
       }
       return Promise.resolve();
     }).then((resp) => {
-      if (process.env.NODE_ENV !== 'development') {
+      if (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') {
         req.log.debug('Retreiving Presigned Url resp: ', JSON.stringify(resp.data));
         return new Promise((accept, reject) => {
           if (resp.status !== 200 || resp.data.result.status !== 200) {
diff --git a/src/routes/attachments/create.spec.js b/src/routes/attachments/create.spec.js
index 0b150f6..e7e0294 100644
--- a/src/routes/attachments/create.spec.js
+++ b/src/routes/attachments/create.spec.js
@@ -7,7 +7,7 @@ import models from '../../models';
 import util from '../../util';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -150,7 +150,7 @@ describe('Project Attachments', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED message when attachment added', (done) => {
+      it('sends send correct BUS API messages when attachment added', (done) => {
         request(server)
           .post(`/v5/projects/${project1.id}/attachments/`)
           .set({
@@ -164,17 +164,27 @@ describe('Project Attachments', () => {
             } else {
               // Wait for app message handler to complete
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED,
-                  sinon.match({ resource: RESOURCES.ATTACHMENT })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED,
-                  sinon.match({ title: body.title })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED,
-                  sinon.match({ description: body.description })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED,
-                  sinon.match({ category: body.category })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED,
-                  sinon.match({ contentType: body.contentType })).should.be.true;
+                createEventSpy.calledThrice.should.be.true;
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_ADDED, sinon.match({
+                  resource: RESOURCES.ATTACHMENT,
+                  title: body.title,
+                  description: body.description,
+                  category: body.category,
+                  contentType: body.contentType,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_FILE_UPLOADED)
+                  .should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, sinon.match({
+                  projectId: project1.id,
+                  projectName: project1.name,
+                  projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                  userId: 40051333,
+                  initiatorUserId: 40051333,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/attachments/delete.js b/src/routes/attachments/delete.js
index e96050f..2e3a821 100644
--- a/src/routes/attachments/delete.js
+++ b/src/routes/attachments/delete.js
@@ -5,6 +5,7 @@ import _ from 'lodash';
 import {
   middleware as tcMiddleware,
 } from 'tc-core-library-js';
+import config from 'config';
 import models from '../../models';
 import util from '../../util';
 import fileService from '../../services/fileService';
@@ -42,7 +43,7 @@ module.exports = [
             .then(() => _attachment.destroy());
         }))
         .then((_attachment) => {
-          if (process.env.NODE_ENV !== 'development') {
+          if (process.env.NODE_ENV !== 'development' || config.get('enableFileUpload') === 'true') {
             return fileService.deleteFile(req, _attachment.filePath);
           }
           return Promise.resolve();
@@ -55,7 +56,6 @@ module.exports = [
             pattachment,
             { correlationId: req.id },
           );
-          // req.app.emit(EVENT.ROUTING_KEY.PROJECT_ATTACHMENT_REMOVED, { req, pattachment });
           // emit the event
           util.sendResourceToKafkaBus(
             req,
diff --git a/src/routes/attachments/delete.spec.js b/src/routes/attachments/delete.spec.js
index 5911254..d31d171 100644
--- a/src/routes/attachments/delete.spec.js
+++ b/src/routes/attachments/delete.spec.js
@@ -9,7 +9,7 @@ import util from '../../util';
 import server from '../../app';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should(); // eslint-disable-line no-unused-vars
 
@@ -182,7 +182,7 @@ describe('Project Attachments delete', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends BUS_API_EVENT.PROJECT_ATTACHMENT_REMOVED message when attachment deleted', (done) => {
+      it('sends send correct BUS API messages  when attachment deleted', (done) => {
         request(server)
           .delete(`/v5/projects/${project1.id}/attachments/${attachment.id}`)
           .set({
@@ -195,11 +195,22 @@ describe('Project Attachments delete', () => {
             } else {
               // Wait for app message handler to complete
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_REMOVED,
-                  sinon.match({ resource: RESOURCES.ATTACHMENT })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_REMOVED,
-                  sinon.match({ id: attachment.id })).should.be.true;
+                createEventSpy.calledTwice.should.be.true;
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_REMOVED, sinon.match({
+                  resource: RESOURCES.ATTACHMENT,
+                  id: attachment.id,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, sinon.match({
+                  projectId: project1.id,
+                  projectName: project1.name,
+                  projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                  userId: 40051333,
+                  initiatorUserId: 40051333,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/attachments/download.js b/src/routes/attachments/download.js
index b3815e5..d925774 100644
--- a/src/routes/attachments/download.js
+++ b/src/routes/attachments/download.js
@@ -61,6 +61,9 @@ module.exports = [
             err.status = 404;
             return Promise.reject(err);
           }
+          if (process.env.NODE_ENV === 'development' && config.get('enableFileUpload') === 'false') {
+            return ['dummy://url'];
+          }
 
           return getFileDownloadUrl(req, attachment.filePath);
         })
@@ -76,6 +79,7 @@ module.exports = [
       return getFileDownloadUrl(req, attachment.filePath);
     })
     .then((result) => {
+      req.log.debug('getFileDownloadUrl result: ', JSON.stringify(result));
       const url = result[1];
       return res.json({ url });
     })
diff --git a/src/routes/attachments/update.spec.js b/src/routes/attachments/update.spec.js
index 4b7c774..34957a4 100644
--- a/src/routes/attachments/update.spec.js
+++ b/src/routes/attachments/update.spec.js
@@ -7,7 +7,7 @@ import models from '../../models';
 import server from '../../app';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -145,7 +145,7 @@ describe('Project Attachments update', () => {
         createEventSpy = sandbox.stub(busApi, 'createEvent');
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_FILES_UPDATED message when attachment updated', (done) => {
+      it('sends send correct BUS API messages when attachment updated', (done) => {
         request(server)
           .patch(`/v5/projects/${project1.id}/attachments/${attachment.id}`)
           .set({
@@ -159,13 +159,23 @@ describe('Project Attachments update', () => {
             } else {
               // Wait for app message handler to complete
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_UPDATED,
-                  sinon.match({ resource: RESOURCES.ATTACHMENT })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_UPDATED,
-                  sinon.match({ title: 'updated title' })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_UPDATED,
-                  sinon.match({ description: 'updated description' })).should.be.true;
+                createEventSpy.calledTwice.should.be.true;
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_ATTACHMENT_UPDATED, sinon.match({
+                  resource: RESOURCES.ATTACHMENT,
+                  title: 'updated title',
+                  description: 'updated description',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_FILES_UPDATED, sinon.match({
+                  projectId: project1.id,
+                  projectName: project1.name,
+                  projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                  userId: 40051333,
+                  initiatorUserId: 40051333,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/form/version/getVersion.js b/src/routes/form/version/getVersion.js
index 345f204..d416715 100644
--- a/src/routes/form/version/getVersion.js
+++ b/src/routes/form/version/getVersion.js
@@ -4,8 +4,10 @@
 import validate from 'express-validation';
 import Joi from 'joi';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
-import models from '../../../models';
+
 import util from '../../../util';
+import models from '../../../models';
+
 
 const permissions = tcMiddleware.permissions;
 
@@ -44,15 +46,7 @@ module.exports = [
     .then((data) => {
       if (data.length === 0) {
         req.log.debug('No form found in ES');
-        models.Form.findOne({
-          where: {
-            key: req.params.key,
-            version: req.params.version,
-          },
-          order: [['revision', 'DESC']],
-          limit: 1,
-          attributes: { exclude: ['deletedAt', 'deletedBy'] },
-        })
+        return models.Form.findOneWithLatestRevision(req.params)
           .then((form) => {
             // Not found
             if (!form) {
@@ -64,10 +58,10 @@ module.exports = [
             return Promise.resolve();
           })
           .catch(next);
-      } else {
-        req.log.debug('forms found in ES');
-        res.json(data[0].inner_hits.forms.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
       }
+      req.log.debug('forms found in ES');
+      res.json(data[0].inner_hits.forms.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
+      return Promise.resolve();
     })
     .catch(next);
   },
diff --git a/src/routes/index.js b/src/routes/index.js
index adb166b..c57e665 100644
--- a/src/routes/index.js
+++ b/src/routes/index.js
@@ -65,6 +65,11 @@ router.route('/v5/projects/metadata/productCategories')
 router.route('/v5/projects/metadata/productCategories/:key')
   .get(require('./productCategories/get'));
 
+router.route('/v5/projects/metadata/workManagementPermission')
+  .get(require('./workManagementPermissions/list'));
+router.route('/v5/projects/metadata/workManagementPermission/:id')
+  .get(require('./workManagementPermissions/get'));
+
 
 router.use('/v5/projects/metadata', compression());
 router.route('/v5/projects/metadata')
@@ -90,6 +95,14 @@ router.route('/v5/projects/:projectId(\\d+)')
   .patch(require('./projects/update'))
   .delete(require('./projects/delete'));
 
+router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests')
+  .post(require('./scopeChangeRequests/create'));
+  // .get(require('./scopeChangeRequests/list'));
+router.route('/v5/projects/:projectId(\\d+)/scopeChangeRequests/:requestId(\\d+)')
+  // .get(require('./scopeChangeRequests/get'))
+  .patch(require('./scopeChangeRequests/update'));
+  // .delete(require('./scopeChangeRequests/delete'));
+
 router.route('/v5/projects/:projectId(\\d+)/members')
   .get(require('./projectMembers/list'))
   .post(require('./projectMembers/create'));
@@ -99,6 +112,10 @@ router.route('/v5/projects/:projectId(\\d+)/members/:id(\\d+)')
   .delete(require('./projectMembers/delete'))
   .patch(require('./projectMembers/update'));
 
+// Permissions
+router.route('/v5/projects/:projectId(\\d+)/permissions')
+  .get(require('./permissions/get'));
+
 router.route('/v5/projects/:projectId(\\d+)/attachments')
   .post(require('./attachments/create'))
   .get(require('./attachments/list'));
@@ -150,6 +167,13 @@ router.route('/v5/projects/metadata/productCategories/:key')
   .patch(require('./productCategories/update'))
   .delete(require('./productCategories/delete'));
 
+router.route('/v5/projects/metadata/workManagementPermission')
+  .post(require('./workManagementPermissions/create'));
+
+router.route('/v5/projects/metadata/workManagementPermission/:id')
+  .patch(require('./workManagementPermissions/update'))
+  .delete(require('./workManagementPermissions/delete'));
+
 router.route('/v5/projects/metadata/projectTypes')
   .post(require('./projectTypes/create'));
 
@@ -264,6 +288,52 @@ router.route('/v5/projects/metadata/planConfig/:key/versions/:version(\\d+)')
   .patch(require('./planConfig/version/update'))
   .delete(require('./planConfig/version/delete'));
 
+// work streams
+router.route('/v5/projects/:projectId(\\d+)/workstreams')
+  .get(require('./workStreams/list'))
+  .post(require('./workStreams/create'));
+
+router.route('/v5/projects/:projectId(\\d+)/workstreams/:id(\\d+)')
+  .get(require('./workStreams/get'))
+  .patch(require('./workStreams/update'))
+  .delete(require('./workStreams/delete'));
+
+// works
+router.route('/v5/projects/:projectId(\\d+)/workstreams/:workStreamId(\\d+)/works')
+  .get(require('./works/list'))
+  .post(require('./works/create'));
+
+router.route('/v5/projects/:projectId(\\d+)/workstreams/:workStreamId(\\d+)/works/:id(\\d+)')
+  .get(require('./works/get'))
+  .patch(require('./works/update'))
+  .delete(require('./works/delete'));
+
+// work items
+router.route('/v5/projects/:projectId(\\d+)/workstreams/:workStreamId(\\d+)/works/:workId(\\d+)/workitems')
+  .get(require('./workItems/list'))
+  .post(require('./workItems/create'));
+
+router.route('/v5/projects/:projectId(\\d+)/workstreams/:workStreamId(\\d+)/works/:workId(\\d+)/workitems/:id(\\d+)')
+  .get(require('./workItems/get'))
+  .patch(require('./workItems/update'))
+  .delete(require('./workItems/delete'));
+
+router.route('/v5/projects/:projectId/reports')
+  .get(require('./projectReports/getReport'));
+
+// Project Settings
+router.route('/v5/projects/:projectId(\\d+)/settings/:id(\\d+)')
+  .patch(require('./projectSettings/update'))
+  .delete(require('./projectSettings/delete'));
+
+router.route('/v5/projects/:projectId(\\d+)/settings')
+  .get(require('./projectSettings/list'))
+  .post(require('./projectSettings/create'));
+
+// Project Estimation Items
+router.route('/v5/projects/:projectId(\\d+)/estimations/:estimationId(\\d+)/items')
+  .get(require('./projectEstimationItems/list'));
+
 // register error handler
 router.use((err, req, res, next) => { // eslint-disable-line no-unused-vars
   // DO NOT REMOVE next arg.. even though eslint
diff --git a/src/routes/metadata/list.js b/src/routes/metadata/list.js
index d2b239c..e254b97 100644
--- a/src/routes/metadata/list.js
+++ b/src/routes/metadata/list.js
@@ -26,6 +26,10 @@ function getUsedModel() {
     priceConfig: { },
   };
   const query = {
+    where: {
+      deletedAt: { $eq: null },
+      disabled: false,
+    },
     attributes: { exclude: ['deletedAt', 'deletedBy'] },
     raw: true,
   };
@@ -73,6 +77,14 @@ module.exports = [
       attributes: { exclude: ['deletedAt', 'deletedBy'] },
       raw: true,
     };
+    const projectProductTemplateQuery = {
+      where: {
+        deletedAt: { $eq: null },
+        disabled: false,
+      },
+      attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      raw: true,
+    };
 
     // when user query with includeAllReferred, return result with all used version of
     // Form, PriceConfig, PlanConfig
@@ -92,8 +104,8 @@ module.exports = [
         }).then((latestVersionModels) => {
           latestVersion = latestVersionModels;
           return Promise.all([
-            models.ProjectTemplate.findAll(query),
-            models.ProductTemplate.findAll(query),
+            models.ProjectTemplate.findAll(projectProductTemplateQuery),
+            models.ProductTemplate.findAll(projectProductTemplateQuery),
             models.MilestoneTemplate.findAll(query),
             models.ProjectType.findAll(query),
             models.ProductCategory.findAll(query),
@@ -116,14 +128,15 @@ module.exports = [
         .catch(next);
     }
     return Promise.all([
-      models.ProjectTemplate.findAll(query),
-      models.ProductTemplate.findAll(query),
+      models.ProjectTemplate.findAll(projectProductTemplateQuery),
+      models.ProductTemplate.findAll(projectProductTemplateQuery),
       models.MilestoneTemplate.findAll(query),
       models.ProjectType.findAll(query),
       models.ProductCategory.findAll(query),
       models.Form.latestVersion(),
       models.PriceConfig.latestVersion(),
       models.PlanConfig.latestVersion(),
+      models.BuildingBlock.findAll(query),
     ])
       .then((results) => {
         res.json({
@@ -135,6 +148,7 @@ module.exports = [
           forms: results[5],
           priceConfigs: results[6],
           planConfigs: results[7],
+          buildingBlocks: results[8],
         });
       })
       .catch(next);
diff --git a/src/routes/metadata/list.spec.js b/src/routes/metadata/list.spec.js
index 762758e..52956d0 100644
--- a/src/routes/metadata/list.spec.js
+++ b/src/routes/metadata/list.spec.js
@@ -26,6 +26,7 @@ const projectTemplates = [
     priceConfig: { key: 'key1', version: 1 },
     createdBy: 1,
     updatedBy: 1,
+    disabled: false,
   },
 ];
 const productTemplates = [
@@ -111,7 +112,7 @@ const forms = [
   {
     key: 'productKey 1',
     config: {
-      questions: [{
+      sections: [{
         id: 'appDefinition',
         title: 'Sample Project',
         required: true,
@@ -178,6 +179,31 @@ const planConfigs = [
   },
 ];
 
+const buildingBlocks = [
+  {
+    key: 'key1',
+    config: {
+      hello: 'world',
+    },
+    privateConfig: {
+      message: 'you should not see this',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    key: 'key2',
+    config: {
+      hello: 'topcoder',
+    },
+    privateConfig: {
+      message: 'you should not see this',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  },
+];
+
 describe('GET all metadata', () => {
   before((done) => {
     testUtil.clearDb()
@@ -188,7 +214,8 @@ describe('GET all metadata', () => {
     .then(() => models.ProductCategory.bulkCreate(productCategories))
     .then(() => models.Form.bulkCreate(forms))
     .then(() => models.PriceConfig.bulkCreate(priceConfigs))
-    .then(() => models.PlanConfig.bulkCreate(planConfigs).then(() => done()));
+    .then(() => models.PlanConfig.bulkCreate(planConfigs))
+    .then(() => models.BuildingBlock.bulkCreate(buildingBlocks).then(() => done()));
   });
 
   after((done) => {
@@ -288,5 +315,29 @@ describe('GET all metadata', () => {
         })
         .expect(200, done);
     });
+
+    it('should return correct building blocks for admin', (done) => {
+      request(server)
+        .get('/v5/projects/metadata')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            should.exist(resJson.buildingBlocks);
+            resJson.buildingBlocks.length.should.be.eql(2);
+            resJson.buildingBlocks[0].key.should.be.eql('key1');
+            should.not.exist(resJson.buildingBlocks[0].privateConfig);
+            resJson.buildingBlocks[1].key.should.be.eql('key2');
+            should.not.exist(resJson.buildingBlocks[1].privateConfig);
+            done();
+          }
+        });
+    });
   });
 });
diff --git a/src/routes/milestoneTemplates/clone.js b/src/routes/milestoneTemplates/clone.js
index d6c21f9..2666287 100644
--- a/src/routes/milestoneTemplates/clone.js
+++ b/src/routes/milestoneTemplates/clone.js
@@ -28,7 +28,7 @@ module.exports = [
   (req, res, next) => {
     let result;
 
-    return models.sequelize.transaction(tx => // eslint-disable-line no-unused-vars
+    return models.sequelize.transaction(() =>
       // Find the product template
       models.MilestoneTemplate.findAll({
         where: {
diff --git a/src/routes/milestoneTemplates/create.js b/src/routes/milestoneTemplates/create.js
index e2fc595..d72f727 100644
--- a/src/routes/milestoneTemplates/create.js
+++ b/src/routes/milestoneTemplates/create.js
@@ -48,9 +48,9 @@ module.exports = [
       updatedBy: req.authUser.userId,
     });
     let result;
-    return models.sequelize.transaction(tx =>
+    return models.sequelize.transaction(() =>
       // Create the milestone template
-      models.MilestoneTemplate.create(entity, { transaction: tx })
+      models.MilestoneTemplate.create(entity)
         .then((createdEntity) => {
           // Omit deletedAt and deletedBy
           result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy');
@@ -64,7 +64,6 @@ module.exports = [
               id: { $ne: result.id },
               order: { $gte: result.order },
             },
-            transaction: tx,
           });
         })
         .then((updatedCount) => {
@@ -77,7 +76,6 @@ module.exports = [
               },
               order: [['updatedAt', 'DESC']],
               limit: updatedCount[0],
-              transaction: tx,
             });
           }
           return Promise.resolve();
diff --git a/src/routes/milestoneTemplates/list.spec.js b/src/routes/milestoneTemplates/list.spec.js
index 74f668a..5a4b156 100644
--- a/src/routes/milestoneTemplates/list.spec.js
+++ b/src/routes/milestoneTemplates/list.spec.js
@@ -113,6 +113,9 @@ const milestoneTemplates = [
 ];
 
 describe('LIST milestone template', () => {
+  before((done) => {
+    testUtil.clearES(done);
+  });
   beforeEach((done) => {
     testUtil.clearDb()
     .then(() => models.ProductTemplate.bulkCreate(productTemplates))
diff --git a/src/routes/milestones/create.js b/src/routes/milestones/create.js
index 1284887..0bbdf54 100644
--- a/src/routes/milestones/create.js
+++ b/src/routes/milestones/create.js
@@ -30,10 +30,10 @@ const schema = {
     type: Joi.string().max(45).required(),
     details: Joi.object(),
     order: Joi.number().integer().required(),
-    plannedText: Joi.string().max(512).required(),
-    activeText: Joi.string().max(512).required(),
-    completedText: Joi.string().max(512).required(),
-    blockedText: Joi.string().max(512).required(),
+    plannedText: Joi.string().max(512),
+    activeText: Joi.string().max(512),
+    completedText: Joi.string().max(512),
+    blockedText: Joi.string().max(512),
     hidden: Joi.boolean().optional(),
     createdAt: Joi.any().strip(),
     updatedAt: Joi.any().strip(),
@@ -58,12 +58,10 @@ module.exports = [
     });
     let result;
 
-    // Validate startDate and endDate to be within the timeline startDate and endDate
+    // Validate startDate is not earlier than timeline startDate
     let error;
     if (req.body.startDate < req.timeline.startDate) {
       error = 'Milestone startDate must not be before the timeline startDate';
-    } else if (req.body.endDate && req.timeline.endDate && req.body.endDate > req.timeline.endDate) {
-      error = 'Milestone endDate must not be after the timeline endDate';
     }
     if (error) {
       const apiErr = new Error(error);
@@ -71,9 +69,9 @@ module.exports = [
       return next(apiErr);
     }
 
-    return models.sequelize.transaction(tx =>
+    return models.sequelize.transaction(() =>
       // Save to DB
-      models.Milestone.create(entity, { transaction: tx })
+      models.Milestone.create(entity)
         .then((createdEntity) => {
           // Omit deletedAt, deletedBy
           result = _.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy');
@@ -86,7 +84,6 @@ module.exports = [
               id: { $ne: result.id },
               order: { $gte: result.order },
             },
-            transaction: tx,
           });
         })
         .then((updatedCount) => {
@@ -99,7 +96,6 @@ module.exports = [
               },
               order: [['updatedAt', 'DESC']],
               limit: updatedCount[0],
-              transaction: tx,
             });
           }
           return Promise.resolve();
@@ -131,7 +127,14 @@ module.exports = [
           req,
           EVENT.ROUTING_KEY.MILESTONE_UPDATED,
           RESOURCES.MILESTONE,
-          _.assign(_.pick(milestone.toJSON(), 'id', 'order', 'updatedBy', 'updatedAt'))),
+          _.assign(_.pick(milestone.toJSON(), 'id', 'order', 'updatedBy', 'updatedAt')),
+          // Pass the same object as original milestone even though, their time has changed.
+          // So far we don't use time properties in the handler so it's ok. But in general, we should pass
+          // the original milestones. <- TODO
+          _.assign(_.pick(milestone.toJSON(), 'id', 'order', 'updatedBy', 'updatedAt')),
+          null, // no route
+          true, // don't send event to Notification Service as the main event here is updating one milestone
+        ),
       );
 
       // Write to the response
diff --git a/src/routes/milestones/create.spec.js b/src/routes/milestones/create.spec.js
index 8d9163b..271e1d2 100644
--- a/src/routes/milestones/create.spec.js
+++ b/src/routes/milestones/create.spec.js
@@ -10,7 +10,7 @@ import server from '../../app';
 import testUtil from '../../tests/util';
 import models from '../../models';
 import busApi from '../../services/busApi';
-import { EVENT, RESOURCES, BUS_API_EVENT } from '../../constants';
+import { EVENT, RESOURCES, BUS_API_EVENT, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -142,11 +142,12 @@ describe('CREATE milestone', () => {
                 // Create milestones
                 models.Milestone.bulkCreate([
                   {
+                    id: 11,
                     timelineId: 1,
                     name: 'milestone 1',
                     duration: 2,
                     startDate: '2018-05-03T00:00:00.000Z',
-                    status: 'open',
+                    status: 'draft',
                     type: 'type1',
                     details: {
                       detail1: {
@@ -164,11 +165,12 @@ describe('CREATE milestone', () => {
                     updatedBy: 2,
                   },
                   {
+                    id: 12,
                     timelineId: 1,
                     name: 'milestone 2',
                     duration: 3,
                     startDate: '2018-05-04T00:00:00.000Z',
-                    status: 'open',
+                    status: 'draft',
                     type: 'type2',
                     order: 2,
                     plannedText: 'plannedText 2',
@@ -179,11 +181,12 @@ describe('CREATE milestone', () => {
                     updatedBy: 3,
                   },
                   {
+                    id: 13,
                     timelineId: 1,
                     name: 'milestone 3',
                     duration: 4,
                     startDate: '2018-05-04T00:00:00.000Z',
-                    status: 'open',
+                    status: 'draft',
                     type: 'type3',
                     order: 3,
                     plannedText: 'plannedText 3',
@@ -212,7 +215,7 @@ describe('CREATE milestone', () => {
       startDate: '2018-05-05T00:00:00.000Z',
       endDate: '2018-05-07T00:00:00.000Z',
       completionDate: '2018-05-08T00:00:00.000Z',
-      status: 'open',
+      status: 'draft',
       type: 'type4',
       details: {
         detail1: {
@@ -309,66 +312,6 @@ describe('CREATE milestone', () => {
         .expect(400, done);
     });
 
-    it('should return 400 if missing plannedText', (done) => {
-      const invalidBody = _.assign({}, body, {
-        plannedText: undefined,
-      });
-
-      request(server)
-        .post('/v5/timelines/1/milestones')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.admin}`,
-        })
-        .send(invalidBody)
-        .expect('Content-Type', /json/)
-        .expect(400, done);
-    });
-
-    it('should return 400 if missing activeText', (done) => {
-      const invalidBody = _.assign({}, body, {
-        activeText: undefined,
-      });
-
-      request(server)
-        .post('/v5/timelines/1/milestones')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.admin}`,
-        })
-        .send(invalidBody)
-        .expect('Content-Type', /json/)
-        .expect(400, done);
-    });
-
-    it('should return 400 if missing completedText', (done) => {
-      const invalidBody = _.assign({}, body, {
-        completedText: undefined,
-      });
-
-      request(server)
-        .post('/v5/timelines/1/milestones')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.admin}`,
-        })
-        .send(invalidBody)
-        .expect('Content-Type', /json/)
-        .expect(400, done);
-    });
-
-    it('should return 400 if missing blockedText', (done) => {
-      const invalidBody = _.assign({}, body, {
-        blockedText: undefined,
-      });
-
-      request(server)
-        .post('/v5/timelines/1/milestones')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.admin}`,
-        })
-        .send(invalidBody)
-        .expect('Content-Type', /json/)
-        .expect(400, done);
-    });
-
     it('should return 400 if startDate is after endDate', (done) => {
       const invalidBody = _.assign({}, body, {
         startDate: '2018-05-29T00:00:00.000Z',
@@ -416,21 +359,6 @@ describe('CREATE milestone', () => {
         .expect(400, done);
     });
 
-    it('should return 400 if endDate is after the timeline endDate', (done) => {
-      const invalidBody = _.assign({}, body, {
-        endDate: '2018-06-13T00:00:00.000Z',
-      });
-
-      request(server)
-        .post('/v5/timelines/1/milestones')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.admin}`,
-        })
-        .send(invalidBody)
-        .expect('Content-Type', /json/)
-        .expect(400, done);
-    });
-
     it('should return 400 if invalid timelineId param', (done) => {
       request(server)
         .post('/v5/timelines/0/milestones')
@@ -499,6 +427,15 @@ describe('CREATE milestone', () => {
           should.not.exist(resJson.deletedBy);
           should.not.exist(resJson.deletedAt);
 
+          // validate statusHistory
+          should.exist(resJson.statusHistory);
+          resJson.statusHistory.should.be.an('array');
+          resJson.statusHistory.length.should.be.eql(1);
+          resJson.statusHistory.forEach((statusHistory) => {
+            statusHistory.reference.should.be.eql('milestone');
+            statusHistory.referenceId.should.be.eql(resJson.id);
+          });
+
           // eslint-disable-next-line no-unused-expressions
           server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.MILESTONE_ADDED).should.be.true;
 
@@ -506,11 +443,11 @@ describe('CREATE milestone', () => {
           models.Milestone.findAll({ where: { timelineId: 1 } })
             .then((milestones) => {
               _.each(milestones, (milestone) => {
-                if (milestone.id === 1) {
+                if (milestone.id === 11) {
                   milestone.order.should.be.eql(1);
-                } else if (milestone.id === 2) {
+                } else if (milestone.id === 12) {
                   milestone.order.should.be.eql(2 + 1);
-                } else if (milestone.id === 3) {
+                } else if (milestone.id === 13) {
                   milestone.order.should.be.eql(3 + 1);
                 }
               });
@@ -605,7 +542,7 @@ describe('CREATE milestone', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.MILESTONE_ADDED when milestone created', (done) => {
+      it('sends send correct BUS API messages milestone created', (done) => {
         request(server)
           .post('/v5/timelines/1/milestones')
           .set({
@@ -619,13 +556,35 @@ describe('CREATE milestone', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.callCount.should.be.eql(3);
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_ADDED,
-                  sinon.match({ resource: RESOURCES.MILESTONE })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_ADDED,
-                  sinon.match({ name: 'milestone 4' })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_ADDED,
-                  sinon.match({ description: 'description 4' })).should.be.true;
+                createEventSpy.callCount.should.be.eql(4);
+
+                // added a new milestone
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_ADDED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  name: 'milestone 4',
+                  description: 'description 4',
+                  order: 2,
+                })).should.be.true;
+
+                // as order of the next milestones after the added one have been updated, we send events about their update
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  order: 3,
+                })).should.be.true;
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  order: 4,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_ADDED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/milestones/delete.js b/src/routes/milestones/delete.js
index 78b9a3e..ade643c 100644
--- a/src/routes/milestones/delete.js
+++ b/src/routes/milestones/delete.js
@@ -30,11 +30,10 @@ module.exports = [
       id: req.params.milestoneId,
     };
 
-    return models.sequelize.transaction(tx =>
+    return models.sequelize.transaction(() =>
       // Find the milestone
       models.Milestone.findOne({
         where,
-        transaction: tx,
       })
         .then((milestone) => {
           // Not found
@@ -45,9 +44,10 @@ module.exports = [
           }
 
           // Update the deletedBy, and soft delete
-          return milestone.update({ deletedBy: req.authUser.userId }, { transaction: tx })
-            .then(() => milestone.destroy({ transaction: tx }));
-        })
+          return milestone.update({ deletedBy: req.authUser.userId })
+            .then(() => milestone.destroy());
+        }),
+    )
     .then((deleted) => {
       // Send event to bus
       req.log.debug('Sending event to RabbitMQ bus for milestone %d', deleted.id);
@@ -67,6 +67,6 @@ module.exports = [
       res.status(204).end();
       return Promise.resolve();
     })
-    .catch(next));
+    .catch(next);
   },
 ];
diff --git a/src/routes/milestones/delete.spec.js b/src/routes/milestones/delete.spec.js
index edbc2fb..a505e0d 100644
--- a/src/routes/milestones/delete.spec.js
+++ b/src/routes/milestones/delete.spec.js
@@ -9,14 +9,13 @@ import chai from 'chai';
 import models from '../../models';
 import server from '../../app';
 import testUtil from '../../tests/util';
-import { EVENT, RESOURCES, BUS_API_EVENT } from '../../constants';
+import { EVENT, RESOURCES, BUS_API_EVENT, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 import busApi from '../../services/busApi';
 
 const should = chai.should(); // eslint-disable-line no-unused-vars
 
 const expectAfterDelete = (timelineId, id, err, next) => {
   if (err) throw err;
-  setTimeout(() =>
   models.Milestone.findOne({
     where: {
       timelineId,
@@ -30,12 +29,21 @@ const expectAfterDelete = (timelineId, id, err, next) => {
       } else {
         chai.assert.isNotNull(res.deletedAt);
         chai.assert.isNotNull(res.deletedBy);
+
+        request(server)
+          .get(`/v5/timelines/${timelineId}/milestones/${id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(404, next);
       }
-      next();
-    }), 500);
+    });
 };
 
 describe('DELETE milestone', () => {
+  before((done) => {
+    testUtil.clearES(done);
+  });
   beforeEach((done) => {
     testUtil.clearDb()
       .then(() => {
@@ -157,6 +165,7 @@ describe('DELETE milestone', () => {
                 // Create milestones
                 models.Milestone.bulkCreate([
                   {
+                    id: 1,
                     timelineId: 1,
                     name: 'milestone 1',
                     duration: 2,
@@ -179,6 +188,7 @@ describe('DELETE milestone', () => {
                     updatedBy: 2,
                   },
                   {
+                    id: 2,
                     timelineId: 1,
                     name: 'milestone 2',
                     duration: 3,
@@ -194,6 +204,7 @@ describe('DELETE milestone', () => {
                     updatedBy: 3,
                   },
                   {
+                    id: 3,
                     timelineId: 1,
                     name: 'milestone 3',
                     duration: 4,
@@ -371,7 +382,7 @@ describe('DELETE milestone', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.MILESTONE_REMOVED when milestone removed', (done) => {
+      it('sends send correct BUS API messages when milestone removed', (done) => {
         request(server)
           .delete('/v5/timelines/1/milestones/1')
           .set({
@@ -383,11 +394,22 @@ describe('DELETE milestone', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_REMOVED,
-                  sinon.match({ resource: RESOURCES.MILESTONE })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_REMOVED,
-                  sinon.match({ id: 1 })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_REMOVED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  id: 1,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_REMOVED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/milestones/get.spec.js b/src/routes/milestones/get.spec.js
index 1c6797d..f541eb0 100644
--- a/src/routes/milestones/get.spec.js
+++ b/src/routes/milestones/get.spec.js
@@ -133,6 +133,7 @@ describe('GET milestone', () => {
                 // Create milestones
                 models.Milestone.bulkCreate([
                   {
+                    id: 1,
                     timelineId: 1,
                     name: 'milestone 1',
                     duration: 2,
@@ -155,6 +156,7 @@ describe('GET milestone', () => {
                     updatedBy: 2,
                   },
                   {
+                    id: 2,
                     timelineId: 1,
                     name: 'milestone 2',
                     duration: 3,
@@ -170,6 +172,7 @@ describe('GET milestone', () => {
                     updatedBy: 3,
                   },
                   {
+                    id: 3,
                     timelineId: 1,
                     name: 'milestone 3',
                     duration: 4,
@@ -303,6 +306,15 @@ describe('GET milestone', () => {
           should.not.exist(resJson.deletedBy);
           should.not.exist(resJson.deletedAt);
 
+          // validate statusHistory
+          should.exist(resJson.statusHistory);
+          resJson.statusHistory.should.be.an('array');
+          resJson.statusHistory.length.should.be.eql(1);
+          resJson.statusHistory.forEach((statusHistory) => {
+            statusHistory.reference.should.be.eql('milestone');
+            statusHistory.referenceId.should.be.eql(resJson.id);
+          });
+
           done();
         });
     });
diff --git a/src/routes/milestones/list.spec.js b/src/routes/milestones/list.spec.js
index 570e426..e5ccab4 100644
--- a/src/routes/milestones/list.spec.js
+++ b/src/routes/milestones/list.spec.js
@@ -3,8 +3,9 @@
  */
 import chai from 'chai';
 import request from 'supertest';
-import sleep from 'sleep';
+// import sleep from 'sleep';
 import config from 'config';
+import _ from 'lodash';
 
 import models from '../../models';
 import server from '../../app';
@@ -50,6 +51,7 @@ const milestones = [
       detail2: [1, 2, 3],
     },
     order: 1,
+    hidden: false,
     plannedText: 'plannedText 1',
     activeText: 'activeText 1',
     completedText: 'completedText 1',
@@ -68,6 +70,7 @@ const milestones = [
     status: 'open',
     type: 'type2',
     order: 2,
+    hidden: false,
     plannedText: 'plannedText 2',
     activeText: 'activeText 2',
     completedText: 'completedText 2',
@@ -162,25 +165,24 @@ describe('LIST milestones', () => {
                   updatedBy: 2,
                 },
               ]))
-              .then(() =>
-                // Create timelines and milestones
-                models.Timeline.bulkCreate(timelines)
-                  .then(() => models.Milestone.bulkCreate(milestones)))
-              .then(() => {
+              // Create timelines and milestones
+              .then(() => models.Timeline.bulkCreate(timelines))
+              .then(() => models.Milestone.bulkCreate(milestones))
+              .then((createdMilestones) => {
                 // Index to ES
-                timelines[0].milestones = milestones;
+                timelines[0].milestones = _.map(createdMilestones, cm => _.omit(cm.toJSON(), 'deletedAt', 'deletedBy'));
                 timelines[0].projectId = 1;
                 return server.services.es.index({
                   index: ES_TIMELINE_INDEX,
                   type: ES_TIMELINE_TYPE,
                   id: timelines[0].id,
                   body: timelines[0],
-                })
-                  .then(() => {
-                    // sleep for some time, let elasticsearch indices be settled
-                    sleep.sleep(5);
-                    done();
-                  });
+                });
+              })
+              .then(() => {
+                // sleep for some time, let elasticsearch indices be settled
+                // sleep.sleep(5);
+                done();
               });
           });
       });
@@ -244,8 +246,18 @@ describe('LIST milestones', () => {
           const resJson = res.body;
           resJson.should.have.length(2);
 
-          resJson[0].should.be.eql(milestones[0]);
-          resJson[1].should.be.eql(milestones[1]);
+          resJson.forEach((milestone, index) => {
+            milestone.statusHistory.should.be.an('array');
+            milestone.statusHistory.length.should.be.eql(1);
+            milestone.statusHistory.forEach((statusHistory) => {
+              statusHistory.reference.should.be.eql('milestone');
+              statusHistory.referenceId.should.be.eql(milestone.id);
+            });
+
+            const m = _.omitBy(_.omit(milestone, ['statusHistory']), _.isNil);
+
+            m.should.be.eql(milestones[index]);
+          });
 
           done();
         });
@@ -320,8 +332,10 @@ describe('LIST milestones', () => {
           const resJson = res.body;
           resJson.should.have.length(2);
 
-          resJson[0].should.be.eql(milestones[1]);
-          resJson[1].should.be.eql(milestones[0]);
+          const m1 = _.omitBy(_.omit(resJson[0], ['statusHistory']), _.isNil);
+          const m2 = _.omitBy(_.omit(resJson[1], ['statusHistory']), _.isNil);
+          m1.should.be.eql(milestones[1]);
+          m2.should.be.eql(milestones[0]);
 
           done();
         });
diff --git a/src/routes/milestones/update.js b/src/routes/milestones/update.js
index 6ed6b47..850d531 100644
--- a/src/routes/milestones/update.js
+++ b/src/routes/milestones/update.js
@@ -4,12 +4,13 @@
 import validate from 'express-validation';
 import _ from 'lodash';
 import moment from 'moment';
+import config from 'config';
 import Joi from 'joi';
 import Sequelize from 'sequelize';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import util from '../../util';
 import validateTimeline from '../../middlewares/validateTimeline';
-import { EVENT, RESOURCES, MILESTONE_STATUS } from '../../constants';
+import { EVENT, RESOURCES, MILESTONE_STATUS, ADMIN_ROLES } from '../../constants';
 import models from '../../models';
 
 const permissions = tcMiddleware.permissions;
@@ -110,6 +111,7 @@ const schema = {
     completedText: Joi.string().max(512).optional(),
     blockedText: Joi.string().max(512).optional(),
     hidden: Joi.boolean().optional(),
+    statusComment: Joi.string().when('status', { is: 'paused', then: Joi.required(), otherwise: Joi.optional() }),
     createdAt: Joi.any().strip(),
     updatedAt: Joi.any().strip(),
     deletedAt: Joi.any().strip(),
@@ -144,13 +146,50 @@ module.exports = [
     return models.sequelize.transaction(() =>
       // Find the milestone
       models.Milestone.findOne({ where })
-        .then((milestone) => {
+        .then(async (milestone) => {
           // Not found
           if (!milestone) {
             const apiErr = new Error(`Milestone not found for milestone id ${req.params.milestoneId}`);
             apiErr.status = 404;
             return Promise.reject(apiErr);
           }
+          const validStatuses = JSON.parse(config.get('VALID_STATUSES_BEFORE_PAUSED'));
+          if (entityToUpdate.status === MILESTONE_STATUS.PAUSED && !validStatuses.includes(milestone.status)) {
+            const validStatutesStr = validStatuses.join(', ');
+            const apiErr = new Error(`Milestone can only be paused from the next statuses: ${validStatutesStr}`);
+            apiErr.status = 400;
+            return Promise.reject(apiErr);
+          }
+
+          if (entityToUpdate.status === 'resume') {
+            if (milestone.status !== MILESTONE_STATUS.PAUSED) {
+              const apiErr = new Error('Milestone status isn\'t paused');
+              apiErr.status = 400;
+              return Promise.reject(apiErr);
+            }
+            const statusHistory = await models.StatusHistory.findAll({
+              where: { referenceId: milestone.id },
+              order: [['createdAt', 'desc'], ['id', 'desc']],
+              attributes: ['status', 'id'],
+              limit: 2,
+              raw: true,
+            });
+            if (statusHistory.length === 2) {
+              entityToUpdate.status = statusHistory[1].status;
+            } else {
+              const apiErr = new Error('No previous status is found');
+              apiErr.status = 500;
+              return Promise.reject(apiErr);
+            }
+          }
+
+          if (entityToUpdate.completionDate || entityToUpdate.actualStartDate) {
+            if (!util.hasPermission({ topcoderRoles: ADMIN_ROLES }, req.authUser)) {
+              const apiErr = new Error('You are not authorised to perform this action');
+              apiErr.status = 403;
+              return Promise.reject(apiErr);
+            }
+          }
 
           if (entityToUpdate.completionDate && entityToUpdate.completionDate < milestone.startDate) {
             const apiErr = new Error('The milestone completionDate should be greater or equal than the startDate.');
@@ -205,7 +244,7 @@ module.exports = [
           }
 
           // Update
-          return milestone.update(entityToUpdate);
+          return milestone.update(entityToUpdate, { comment: entityToUpdate.statusComment });
         })
         .then((updatedMilestone) => {
           // Omit deletedAt, deletedBy
@@ -295,12 +334,13 @@ module.exports = [
       );
 
       // emit the event
-      util.sendResourceToKafkaBus(
+      // we cannot use `util.sendResourceToKafkaBus` as we have to pass a custom param `cascadedUpdates`
+      req.app.emit(EVENT.ROUTING_KEY.MILESTONE_UPDATED, {
         req,
-        EVENT.ROUTING_KEY.MILESTONE_UPDATED,
-        RESOURCES.MILESTONE,
-        _.assign(entityToUpdate, _.pick(updated, 'id', 'updatedAt')),
-      );
+        resource: _.assign({ resource: RESOURCES.MILESTONE }, updated),
+        originalResource: _.assign({ resource: RESOURCES.MILESTONE }, original),
+        cascadedUpdates,
+      });
 
       // Write to response
       res.json(updated);
diff --git a/src/routes/milestones/update.spec.js b/src/routes/milestones/update.spec.js
index 056e884..bf8b1c7 100644
--- a/src/routes/milestones/update.spec.js
+++ b/src/routes/milestones/update.spec.js
@@ -11,7 +11,7 @@ import models from '../../models';
 import server from '../../app';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { EVENT, RESOURCES, MILESTONE_STATUS, BUS_API_EVENT } from '../../constants';
+import { EVENT, RESOURCES, MILESTONE_STATUS, BUS_API_EVENT, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -141,7 +141,7 @@ describe('UPDATE Milestone', () => {
                     startDate: '2018-05-13T00:00:00.000Z',
                     endDate: '2018-05-14T00:00:00.000Z',
                     completionDate: '2018-05-15T00:00:00.000Z',
-                    status: 'open',
+                    status: 'active',
                     type: 'type1',
                     details: {
                       detail1: {
@@ -166,7 +166,7 @@ describe('UPDATE Milestone', () => {
                     name: 'Milestone 2',
                     duration: 3,
                     startDate: '2018-05-14T00:00:00.000Z',
-                    status: 'open',
+                    status: 'reviewed',
                     type: 'type2',
                     order: 2,
                     plannedText: 'plannedText 2',
@@ -184,7 +184,7 @@ describe('UPDATE Milestone', () => {
                     name: 'Milestone 3',
                     duration: 3,
                     startDate: '2018-05-14T00:00:00.000Z',
-                    status: 'open',
+                    status: 'active',
                     type: 'type3',
                     order: 3,
                     plannedText: 'plannedText 3',
@@ -202,7 +202,7 @@ describe('UPDATE Milestone', () => {
                     name: 'Milestone 4',
                     duration: 3,
                     startDate: '2018-05-14T00:00:00.000Z',
-                    status: 'open',
+                    status: 'active',
                     type: 'type4',
                     order: 4,
                     plannedText: 'plannedText 4',
@@ -220,7 +220,7 @@ describe('UPDATE Milestone', () => {
                     name: 'Milestone 5',
                     duration: 3,
                     startDate: '2018-05-14T00:00:00.000Z',
-                    status: 'open',
+                    status: 'active',
                     type: 'type5',
                     order: 5,
                     plannedText: 'plannedText 5',
@@ -239,7 +239,7 @@ describe('UPDATE Milestone', () => {
                     name: 'Milestone 6',
                     duration: 3,
                     startDate: '2018-05-14T00:00:00.000Z',
-                    status: 'open',
+                    status: 'active',
                     type: 'type5',
                     order: 1,
                     plannedText: 'plannedText 6',
@@ -265,9 +265,8 @@ describe('UPDATE Milestone', () => {
     const body = {
       name: 'Milestone 1-updated',
       duration: 3,
-      completionDate: '2018-05-16T00:00:00.000Z',
       description: 'description-updated',
-      status: 'closed',
+      status: 'draft',
       type: 'type1-updated',
       details: {
         detail1: {
@@ -302,6 +301,30 @@ describe('UPDATE Milestone', () => {
         .expect(403, done);
     });
 
+    it('should return 403 for non-admin member updating the completionDate', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.completionDate = '2019-01-16T00:00:00.000Z';
+      request(server)
+        .patch('/v5/timelines/1/milestones/1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(newBody)
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-admin member updating the actualStartDate', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.actualStartDate = '2018-05-15T00:00:00.000Z';
+      request(server)
+        .patch('/v5/timelines/1/milestones/1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(newBody)
+        .expect(403, done);
+    });
+
     it('should return 404 for non-existed timeline', (done) => {
       request(server)
         .patch('/v5/timelines/1234/milestones/1')
@@ -488,12 +511,14 @@ describe('UPDATE Milestone', () => {
     });
 
     it('should return 200 for admin', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.completionDate = '2018-05-15T00:00:00.000Z';
       request(server)
         .patch('/v5/timelines/1/milestones/1')
         .set({
           Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
-        .send(body)
+        .send(newBody)
         .expect(200)
         .end((err, res) => {
           const resJson = res.body;
@@ -501,7 +526,7 @@ describe('UPDATE Milestone', () => {
           resJson.name.should.be.eql(body.name);
           resJson.description.should.be.eql(body.description);
           resJson.duration.should.be.eql(body.duration);
-          resJson.completionDate.should.be.eql(body.completionDate);
+          resJson.completionDate.should.be.eql(newBody.completionDate);
           resJson.status.should.be.eql(body.status);
           resJson.type.should.be.eql(body.type);
           resJson.details.should.be.eql({
@@ -522,6 +547,15 @@ describe('UPDATE Milestone', () => {
           should.not.exist(resJson.deletedBy);
           should.not.exist(resJson.deletedAt);
 
+          // validate statusHistory
+          should.exist(resJson.statusHistory);
+          resJson.statusHistory.should.be.an('array');
+          resJson.statusHistory.length.should.be.eql(2);
+          resJson.statusHistory.forEach((statusHistory) => {
+            statusHistory.reference.should.be.eql('milestone');
+            statusHistory.referenceId.should.be.eql(resJson.id);
+          });
+
           // eslint-disable-next-line no-unused-expressions
           server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.MILESTONE_UPDATED).should.be.true;
 
@@ -711,7 +745,7 @@ describe('UPDATE Milestone', () => {
           name: 'Milestone 7',
           duration: 3,
           startDate: '2018-05-14T00:00:00.000Z',
-          status: 'open',
+          status: 'active',
           type: 'type7',
           order: 3,
           plannedText: 'plannedText 7',
@@ -729,7 +763,7 @@ describe('UPDATE Milestone', () => {
           name: 'Milestone 8',
           duration: 3,
           startDate: '2018-05-14T00:00:00.000Z',
-          status: 'open',
+          status: 'active',
           type: 'type7',
           order: 4,
           plannedText: 'plannedText 8',
@@ -784,7 +818,7 @@ describe('UPDATE Milestone', () => {
           name: 'Milestone 7',
           duration: 3,
           startDate: '2018-05-14T00:00:00.000Z',
-          status: 'open',
+          status: 'active',
           type: 'type7',
           order: 2,
           plannedText: 'plannedText 7',
@@ -802,7 +836,7 @@ describe('UPDATE Milestone', () => {
           name: 'Milestone 8',
           duration: 3,
           startDate: '2018-05-14T00:00:00.000Z',
-          status: 'open',
+          status: 'active',
           type: 'type7',
           order: 4,
           plannedText: 'plannedText 8',
@@ -1050,6 +1084,30 @@ describe('UPDATE Milestone', () => {
         .end(done);
     });
 
+    it('should return 200 for admin updating the completionDate', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.completionDate = '2018-05-16T00:00:00.000Z';
+      request(server)
+        .patch('/v5/timelines/1/milestones/1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(newBody)
+        .expect(200, done);
+    });
+
+    it('should return 200 for admin updating the actualStartDate', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.actualStartDate = '2018-05-15T00:00:00.000Z';
+      request(server)
+        .patch('/v5/timelines/1/milestones/1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(newBody)
+        .expect(200, done);
+    });
+
     it('should return 200 for connect manager', (done) => {
       request(server)
         .patch('/v5/timelines/1/milestones/1')
@@ -1083,6 +1141,146 @@ describe('UPDATE Milestone', () => {
         .end(done);
     });
 
+    it('should return 400 if try to pause and statusComment is missed', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.status = 'paused';
+      request(server)
+      .patch('/v5/timelines/1/milestones/1')
+      .set({
+        Authorization: `Bearer ${testUtil.jwts.admin}`,
+      })
+      .send(newBody)
+      .expect(400, done);
+    });
+
+    it('should return 400 if try to pause not active milestone', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.status = 'paused';
+      newBody.statusComment = 'milestone paused';
+      request(server)
+      .patch('/v5/timelines/1/milestones/2')
+      .set({
+        Authorization: `Bearer ${testUtil.jwts.admin}`,
+      })
+      .send(newBody)
+      .expect(400, done);
+    });
+
+    it('should return 200 if try to pause and should have one status history created', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.status = 'paused';
+      newBody.statusComment = 'milestone paused';
+      request(server)
+      .patch('/v5/timelines/1/milestones/1')
+      .set({
+        Authorization: `Bearer ${testUtil.jwts.admin}`,
+      })
+      .send(newBody)
+      .expect(200)
+      .end((err) => {
+        if (err) {
+          done(err);
+        } else {
+          models.Milestone.findByPk(1).then((milestone) => {
+            milestone.status.should.be.eql('paused');
+            return models.StatusHistory.findAll({
+              where: {
+                reference: 'milestone',
+                referenceId: milestone.id,
+                status: milestone.status,
+                comment: 'milestone paused',
+              },
+              paranoid: false,
+            }).then((statusHistories) => {
+              statusHistories.length.should.be.eql(1);
+              done();
+            });
+          });
+        }
+      });
+    });
+
+    it('should return 400 if try to resume not paused milestone', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.status = 'resume';
+      request(server)
+      .patch('/v5/timelines/1/milestones/2')
+      .set({
+        Authorization: `Bearer ${testUtil.jwts.admin}`,
+      })
+      .send(newBody)
+      .expect(400, done);
+    });
+
+    it('should return 200 if try to resume then status should update to last status and ' +
+        'should have one status history created', (done) => {
+      const newBody = _.cloneDeep(body);
+      newBody.status = 'resume';
+      newBody.statusComment = 'new comment';
+      models.Milestone.bulkCreate([
+        {
+          id: 7,
+          timelineId: 1,
+          name: 'Milestone 1 [paused]',
+          duration: 2,
+          startDate: '2018-05-13T00:00:00.000Z',
+          endDate: '2018-05-14T00:00:00.000Z',
+          completionDate: '2018-05-16T00:00:00.000Z',
+          status: 'active',
+          type: 'type1',
+          details: {
+            detail1: {
+              subDetail1A: 1,
+              subDetail1B: 2,
+            },
+            detail2: [1, 2, 3],
+          },
+          order: 1,
+          plannedText: 'plannedText 1',
+          activeText: 'activeText 1',
+          completedText: 'completedText 1',
+          blockedText: 'blockedText 1',
+          createdBy: 1,
+          updatedBy: 2,
+          createdAt: '2018-05-11T00:00:00.000Z',
+          updatedAt: '2018-05-11T00:00:00.000Z',
+        },
+      ]).then(() => models.Milestone.findByPk(7)
+        // pause milestone before resume
+        .then(milestone => milestone.update(_.assign({}, milestone.toJSON(), { status: 'paused' }))),
+      ).then(() => {
+        request(server)
+        .patch('/v5/timelines/1/milestones/7')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(newBody)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            models.Milestone.findByPk(7).then((milestone) => {
+              milestone.status.should.be.eql('active');
+
+              return models.StatusHistory.findAll({
+                where: {
+                  reference: 'milestone',
+                  referenceId: milestone.id,
+                  status: 'active',
+                  comment: 'new comment',
+                },
+                paranoid: false,
+              }).then((statusHistories) => {
+                statusHistories.length.should.be.eql(1);
+                done();
+              }).catch(done);
+            }).catch(done);
+          }
+        });
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -1100,14 +1298,13 @@ describe('UPDATE Milestone', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.MILESTONE_UPDATED when milestone duration updated', (done) => {
+      it('sends send correct BUS API messages when milestone details updated and waiting for customer', (done) => {
         request(server)
           .patch('/v5/timelines/1/milestones/1')
           .set({
             Authorization: `Bearer ${testUtil.jwts.copilot}`,
           })
           .send({
-            // duration: 1,
             details: {
               metadata: { waitingForCustomer: true },
             },
@@ -1118,18 +1315,26 @@ describe('UPDATE Milestone', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                // 5 milestones in total, so it would trigger 5 events
-                // 4 MILESTONE_UPDATED events are for 4 non deleted milestones
-                // 1 TIMELINE_ADJUSTED event, because timeline's end date updated
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({ resource: RESOURCES.MILESTONE })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({
-                    details: {
-                      metadata: { waitingForCustomer: true },
-                    },
-                  })).should.be.true;
+                createEventSpy.callCount.should.be.eql(3);
+
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  details: {
+                    metadata: { waitingForCustomer: true },
+                  },
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_WAITING_CUSTOMER)
+                  .should.be.true;
+
                 done();
               });
             }
@@ -1191,7 +1396,7 @@ describe('UPDATE Milestone', () => {
           });
       });
 
-      it('should ONLY send message BUS_API_EVENT.MILESTONE_UPDATED when milestone order updated', (done) => {
+      it('should send correct BUS API messages when milestone order updated', (done) => {
         request(server)
           .patch('/v5/timelines/1/milestones/1')
           .set({
@@ -1206,18 +1411,29 @@ describe('UPDATE Milestone', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({ resource: RESOURCES.MILESTONE })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({ order: 2 })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  order: 2,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('should ONLY send message BUS_API_EVENT.MILESTONE_UPDATED when milestone plannedText updated', (done) => {
+      it('should send correct BUS API messages when milestone plannedText updated', (done) => {
         request(server)
           .patch('/v5/timelines/1/milestones/1')
           .set({
@@ -1232,11 +1448,22 @@ describe('UPDATE Milestone', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({ resource: RESOURCES.MILESTONE })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED,
-                  sinon.match({ plannedText: 'new text' })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.MILESTONE_UPDATED, sinon.match({
+                  resource: RESOURCES.MILESTONE,
+                  plannedText: 'new text',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MILESTONE_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/orgConfig/list.js b/src/routes/orgConfig/list.js
index b77b3dc..6e5fb74 100644
--- a/src/routes/orgConfig/list.js
+++ b/src/routes/orgConfig/list.js
@@ -3,6 +3,7 @@
  */
 import validate from 'express-validation';
 import Joi from 'joi';
+import _ from 'lodash';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import models from '../../models';
 import util from '../../util';
@@ -20,24 +21,51 @@ module.exports = [
   validate(schema),
   permissions('orgConfig.view'),
   (req, res, next) => {
-    util.fetchFromES('orgConfigs')
-    .then((data) => {
-      // handle filters
-      const filters = req.query;
-      // Throw error if orgId is not present in filter
-      if (!filters.orgId) {
-        next(util.buildApiError('Missing filter orgId', 400));
-      }
-      if (!util.isValidFilter(filters, ['orgId', 'configName'])) {
-        util.handleError('Invalid filters', null, req, next);
-      }
-      req.log.debug(filters);
+    // handle filters
+    const filters = req.query;
+    // Throw error if orgId is not present in filter
+    if (!filters.orgId) {
+      next(util.buildApiError('Missing filter orgId', 400));
+    }
+    if (!util.isValidFilter(filters, ['orgId', 'configName'])) {
+      util.handleError('Invalid filters', null, req, next);
+    }
+    req.log.debug(filters);
+    const orgIds = filters.orgId.split(',');
 
+    // build filter query for ES
+    const must = [{
+      terms: {
+        'orgConfigs.orgId': orgIds,
+      },
+    }];
+    if (filters.configName) {
+      must.push({
+        term: {
+          'orgConfigs.configName': filters.configName,
+        },
+      });
+    }
+
+    util.fetchFromES('orgConfigs', {
+      query: {
+        nested: {
+          path: 'orgConfigs',
+          query: {
+            bool: {
+              must,
+            },
+          },
+          inner_hits: {},
+        },
+      },
+    }, 'metadata')
+    .then((data) => {
       if (data.orgConfigs.length === 0) {
         req.log.debug('No orgConfig found in ES');
 
         // Get all organization config
-        const where = filters || {};
+        const where = filters ? _.assign({}, filters, { orgId: { $in: orgIds } }) : {};
         models.OrgConfig.findAll({
           where,
           attributes: { exclude: ['deletedAt', 'deletedBy'] },
@@ -49,7 +77,7 @@ module.exports = [
         .catch(next);
       } else {
         req.log.debug('orgConfigs found in ES');
-        res.json(data.orgConfigs);
+        res.json(data.orgConfigs.hits.hits.map(hit => hit._source)); // eslint-disable-line no-underscore-dangle
       }
     });
   },
diff --git a/src/routes/orgConfig/list.spec.js b/src/routes/orgConfig/list.spec.js
index 6931742..498f118 100644
--- a/src/routes/orgConfig/list.spec.js
+++ b/src/routes/orgConfig/list.spec.js
@@ -3,6 +3,8 @@
  */
 import chai from 'chai';
 import request from 'supertest';
+import config from 'config';
+import _ from 'lodash';
 
 import models from '../../models';
 import server from '../../app';
@@ -10,6 +12,21 @@ import testUtil from '../../tests/util';
 
 const should = chai.should();
 
+const ES_ORGCONFIG_INDEX = config.get('elasticsearchConfig.metadataIndexName');
+const ES_ORGCONFIG_TYPE = config.get('elasticsearchConfig.metadataDocType');
+
+const validateOrgConfig = (resJson, orgConfig) => {
+  resJson.id.should.be.eql(orgConfig.id);
+  resJson.orgId.should.be.eql(orgConfig.orgId);
+  resJson.configName.should.be.eql(orgConfig.configName);
+  resJson.configValue.should.be.eql(orgConfig.configValue);
+  should.exist(resJson.createdAt);
+  resJson.updatedBy.should.be.eql(orgConfig.updatedBy);
+  should.exist(resJson.updatedAt);
+  should.not.exist(resJson.deletedBy);
+  should.not.exist(resJson.deletedAt);
+};
+
 describe('LIST organization config', () => {
   const orgConfigPath = '/v5/projects/metadata/orgConfig';
   const configs = [
@@ -23,17 +40,50 @@ describe('LIST organization config', () => {
     },
     {
       id: 2,
-      orgId: 'ORG1',
+      orgId: 'ORG2',
       configName: 'project_catalog_url',
       configValue: '/projects/2',
       createdBy: 1,
       updatedBy: 1,
     },
+    {
+      id: 3,
+      orgId: 'ORG3',
+      configName: 'project_catalog_url',
+      configValue: '/projects/3',
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      id: 4,
+      orgId: 'ORG4',
+      configName: 'project_catalog_url',
+      configValue: '/projects/4',
+      createdBy: 1,
+      updatedBy: 1,
+    },
   ];
 
   beforeEach((done) => {
     testUtil.clearDb()
-      .then(() => models.OrgConfig.bulkCreate(configs).then(() => done()));
+      .then(() => models.OrgConfig.bulkCreate(configs, { returning: true }),
+        ).then(async (createdConfigs) => {
+          // Index to ES only orgConfigs with id: 3 and 4
+          const indexedConfigs = _(createdConfigs).filter(createdConfig => createdConfig.toJSON().id > 2)
+            .map((filteredConfig) => {
+              const orgConfigJson = _.omit(filteredConfig.toJSON(), 'deletedAt', 'deletedBy');
+              return orgConfigJson;
+            }).value();
+
+          await server.services.es.index({
+            index: ES_ORGCONFIG_INDEX,
+            type: ES_ORGCONFIG_TYPE,
+            body: {
+              orgConfigs: indexedConfigs,
+            },
+          });
+          done();
+        });
   });
   after((done) => {
     testUtil.clearDb(done);
@@ -48,19 +98,79 @@ describe('LIST organization config', () => {
         })
         .expect(200)
         .end((err, res) => {
-          const config = configs[0];
+          const config1 = configs[0];
+
+          const resJson = res.body;
+          resJson.should.have.length(1);
+          validateOrgConfig(resJson[0], config1);
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin with filter (ES)', (done) => {
+      request(server)
+        .get(`${orgConfigPath}?orgId=${configs[2].orgId}&configName=${configs[2].configName}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const config3 = configs[2];
 
           const resJson = res.body;
           resJson.should.have.length(1);
-          resJson[0].id.should.be.eql(config.id);
-          resJson[0].orgId.should.be.eql(config.orgId);
-          resJson[0].configName.should.be.eql(config.configName);
-          resJson[0].configValue.should.be.eql(config.configValue);
-          should.exist(resJson[0].createdAt);
-          resJson[0].updatedBy.should.be.eql(config.updatedBy);
-          should.exist(resJson[0].updatedAt);
-          should.not.exist(resJson[0].deletedBy);
-          should.not.exist(resJson[0].deletedAt);
+          validateOrgConfig(resJson[0], config3);
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin and filter by multiple orgId (DB)', (done) => {
+      request(server)
+        .get(`${orgConfigPath}?orgId=${configs[0].orgId},${configs[1].orgId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const config1 = configs[0];
+          const config2 = configs[1];
+
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson.forEach((result) => {
+            if (result.id === 1) {
+              validateOrgConfig(result, config1);
+            } else {
+              validateOrgConfig(result, config2);
+            }
+          });
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin and filter by multiple orgId (ES)', (done) => {
+      request(server)
+        .get(`${orgConfigPath}?orgId=${configs[2].orgId},${configs[3].orgId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const config3 = configs[2];
+          const config4 = configs[3];
+
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson.forEach((result) => {
+            if (result.id === 3) {
+              validateOrgConfig(result, config3);
+            } else {
+              validateOrgConfig(result, config4);
+            }
+          });
 
           done();
         });
diff --git a/src/routes/orgConfig/update.spec.js b/src/routes/orgConfig/update.spec.js
index 4921c58..f25c98b 100644
--- a/src/routes/orgConfig/update.spec.js
+++ b/src/routes/orgConfig/update.spec.js
@@ -87,6 +87,9 @@ describe('UPDATE organization config', () => {
 
     it('should return 404 for deleted config', (done) => {
       models.OrgConfig.destroy({ where: { id } })
+        // we should clear ES, otherwise deleted config would be returned by ES
+        // TODO we should create an alternative way to test it, as all the data is "cached" in ES now
+        .then(() => testUtil.clearES())
         .then(() => {
           request(server)
             .patch(`/v5/projects/metadata/orgConfig/${id}`)
diff --git a/src/routes/permissions/get.js b/src/routes/permissions/get.js
new file mode 100644
index 0000000..d22acbe
--- /dev/null
+++ b/src/routes/permissions/get.js
@@ -0,0 +1,65 @@
+/**
+ * API to get project permissions
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('permissions.view'),
+  (req, res, next) => {
+    const projectId = req.params.projectId;
+    return models.Project.findOne({
+      where: {
+        id: projectId,
+      },
+    })
+      .then((project) => {
+        if (!project) {
+          const apiErr = new Error(`Project not found for id ${projectId}`);
+          apiErr.status = 404;
+          return Promise.reject(apiErr);
+        }
+
+        if (!project.templateId) {
+          return Promise.resolve([]);
+        }
+
+        return models.WorkManagementPermission.findAll({
+          where: {
+            projectTemplateId: project.templateId,
+          },
+        });
+      })
+      .then((workManagementPermissions) => {
+        const allowPermissions = {};
+
+        // find all allowed permissions
+        workManagementPermissions.forEach((workManagementPermission) => {
+          const isAllowed = util.hasPermission(
+            workManagementPermission.permission,
+            req.authUser,
+            req.context.currentProjectMembers,
+          );
+
+          if (isAllowed) {
+            allowPermissions[workManagementPermission.policy] = true;
+          }
+        });
+
+        res.json(allowPermissions);
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/permissions/get.spec.js b/src/routes/permissions/get.spec.js
new file mode 100644
index 0000000..57f7d62
--- /dev/null
+++ b/src/routes/permissions/get.spec.js
@@ -0,0 +1,233 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for get.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+chai.should();
+
+describe('GET permissions', () => {
+  let projectId;
+  let templateId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  const permissions = [
+    {
+      policy: 'work.create',
+      permission: {
+        allowRule: {
+          projectRoles: ['customer', 'copilot'],
+          topcoderRoles: ['Connect Manager', 'administrator'],
+        },
+        denyRule: { projectRoles: ['copilot'] },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      policy: 'work.edit',
+      permission: {
+        allowRule: {
+          projectRoles: ['copilot'],
+          topcoderRoles: ['Connect Manager'],
+        },
+        denyRule: { topcoderRoles: ['Connect Admin'] },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ];
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((t) => {
+          templateId = t.id;
+          // Create projects
+          models.Project.create({
+            type: 'generic',
+            billingAccountId: 1,
+            name: 'test1',
+            description: 'test project1',
+            status: 'draft',
+            templateId,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then((project) => {
+            projectId = project.id;
+            models.ProjectMember.bulkCreate([{
+              id: 1,
+              userId: copilotUser.userId,
+              projectId,
+              role: 'copilot',
+              isPrimary: false,
+              createdBy: 1,
+              updatedBy: 1,
+            }, {
+              id: 2,
+              userId: memberUser.userId,
+              projectId,
+              role: 'customer',
+              isPrimary: true,
+              createdBy: 1,
+              updatedBy: 1,
+            }]).then(() => {
+              const newPermissions = _.map(permissions, p => _.assign({}, p, { projectTemplateId: templateId }));
+              models.WorkManagementPermission.bulkCreate(newPermissions, { returning: true })
+                .then(() => done());
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/permissions', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-member', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed project', (done) => {
+      request(server)
+        .get('/v5/projects/9999/permissions')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 200 for project with no template', (done) => {
+      models.Project.create({
+        type: 'generic',
+        name: 'test1',
+        status: 'draft',
+        createdBy: 1,
+        updatedBy: 1,
+        lastActivityAt: 1,
+        lastActivityUserId: '1',
+      })
+        .then((p) => {
+          request(server)
+            .get(`/v5/projects/${p.id}/permissions`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(200)
+            .end((err, res) => {
+              const resJson = res.body;
+              resJson.should.be.empty;
+              done();
+            });
+        });
+    });
+
+    it('should return 200 for connect admin - no permission', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.not.have.all.keys(permissions[0].policy, permissions[1].policy);
+          done();
+        });
+    });
+
+    it('should return 200 for copilot - has both no-permission and permission', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.all.keys(permissions[1].policy);
+          resJson.should.not.have.all.keys(permissions[0].policy);
+          done();
+        });
+    });
+
+    it('should return 200 for admin - has both permission and no-permission', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.all.keys(permissions[0].policy);
+          resJson.should.not.have.all.keys(permissions[1].policy);
+          done();
+        });
+    });
+
+    it('should return 200 for manager - has permissions', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/permissions`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.all.keys(permissions[0].policy, permissions[1].policy);
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/phaseProducts/create.spec.js b/src/routes/phaseProducts/create.spec.js
index 1e51d38..e987ed4 100644
--- a/src/routes/phaseProducts/create.spec.js
+++ b/src/routes/phaseProducts/create.spec.js
@@ -7,6 +7,7 @@ import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
+import { RESOURCES, BUS_API_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -177,7 +178,7 @@ describe('Phase Products', () => {
       request(server)
         .post(`/v5/projects/99999/phases/${phaseId}/products`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .send(body)
         .expect('Content-Type', /json/)
@@ -188,7 +189,7 @@ describe('Phase Products', () => {
       request(server)
         .post(`/v5/projects/${projectId}/phases/99999/products`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .send(body)
         .expect('Content-Type', /json/)
@@ -220,6 +221,68 @@ describe('Phase Products', () => {
         });
     });
 
+    it('should return 201 if requested by admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/phases/${phaseId}/products`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end(done);
+    });
+
+    it('should return 201 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectId}/phases/${phaseId}/products`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(body)
+          .expect('Content-Type', /json/)
+          .expect(201)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/phases/${phaseId}/products`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectId}/phases/${phaseId}/products`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.copilot}`,
+          })
+          .send(body)
+          .expect('Content-Type', /json/)
+          .expect(403)
+          .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -237,7 +300,7 @@ describe('Phase Products', () => {
         sandbox.restore();
       });
 
-      it('should not send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_ADDED when product phase created', (done) => {
+      it('should send correct BUS API messages when product phase created', (done) => {
         request(server)
         .post(`/v5/projects/${projectId}/phases/${phaseId}/products`)
         .set({
@@ -251,7 +314,12 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
+                createEventSpy.callCount.should.be.eql(1);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/phaseProducts/delete.spec.js b/src/routes/phaseProducts/delete.spec.js
index dbbd677..c6e7c35 100644
--- a/src/routes/phaseProducts/delete.spec.js
+++ b/src/routes/phaseProducts/delete.spec.js
@@ -7,6 +7,7 @@ import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
+import { BUS_API_EVENT, RESOURCES } from '../../constants';
 
 const should = chai.should(); // eslint-disable-line no-unused-vars
 
@@ -152,7 +153,7 @@ describe('Phase Products', () => {
       request(server)
         .delete(`/v5/projects/999/phases/${phaseId}/products/${productId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .expect('Content-Type', /json/)
         .expect(404, done);
@@ -162,7 +163,7 @@ describe('Phase Products', () => {
       request(server)
         .delete(`/v5/projects/${projectId}/phases/99999/products/${productId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .expect('Content-Type', /json/)
         .expect(404, done);
@@ -172,7 +173,7 @@ describe('Phase Products', () => {
       request(server)
         .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/99999`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .expect('Content-Type', /json/)
         .expect(404, done);
@@ -188,6 +189,60 @@ describe('Phase Products', () => {
         .end(err => expectAfterDelete(projectId, phaseId, productId, err, done));
     });
 
+    it('should return 204 if requested by admin', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(204)
+        .end(done);
+    });
+
+    it('should return 204 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .expect(204)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+        .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403)
+        .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -205,7 +260,7 @@ describe('Phase Products', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_REMOVED when product phase removed', (done) => {
+      it('should send correct BUS API messages when product phase removed', (done) => {
         request(server)
           .delete(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -217,7 +272,12 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
+                createEventSpy.callCount.should.be.eql(1);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/phaseProducts/list.spec.js b/src/routes/phaseProducts/list.spec.js
index 7a5e39d..3a0fc37 100644
--- a/src/routes/phaseProducts/list.spec.js
+++ b/src/routes/phaseProducts/list.spec.js
@@ -1,7 +1,7 @@
 /* eslint-disable no-unused-expressions */
 import _ from 'lodash';
 import request from 'supertest';
-import sleep from 'sleep';
+// import sleep from 'sleep';
 import chai from 'chai';
 import config from 'config';
 import server from '../../app';
@@ -111,7 +111,7 @@ describe('Phase Products', () => {
                   body: project,
                 }).then(() => {
                   // sleep for some time, let elasticsearch indices be settled
-                  sleep.sleep(5);
+                  // sleep.sleep(5);
                   done();
                 });
               });
diff --git a/src/routes/phaseProducts/update.js b/src/routes/phaseProducts/update.js
index c166837..7ae577d 100644
--- a/src/routes/phaseProducts/update.js
+++ b/src/routes/phaseProducts/update.js
@@ -5,7 +5,7 @@ import Joi from 'joi';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import models from '../../models';
 import util from '../../util';
-import { EVENT, RESOURCES } from '../../constants';
+import { EVENT, RESOURCES, ROUTES } from '../../constants';
 
 
 const permissions = tcMiddleware.permissions;
@@ -78,7 +78,9 @@ module.exports = [
         req,
         EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED,
         RESOURCES.PHASE_PRODUCT,
-        _.assign(updatedProps, _.pick(updated, 'id', 'updatedAt')));
+        updatedValue,
+        previousValue,
+        ROUTES.PHASE_PRODUCTS.UPDATE);
 
       res.json(updated);
     }).catch(err => next(err));
diff --git a/src/routes/phaseProducts/update.spec.js b/src/routes/phaseProducts/update.spec.js
index df8c567..be89221 100644
--- a/src/routes/phaseProducts/update.spec.js
+++ b/src/routes/phaseProducts/update.spec.js
@@ -7,7 +7,7 @@ import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -51,7 +51,7 @@ describe('Phase Products', () => {
     lastName: 'lName',
     email: 'some@abc.com',
   };
-  before((done) => {
+  beforeEach((done) => {
     // mocks
     testUtil.clearDb()
         .then(() => {
@@ -144,7 +144,7 @@ describe('Phase Products', () => {
       request(server)
         .patch(`/v5/projects/999/phases/${phaseId}/products/${productId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
         })
         .send(updateBody)
         .expect('Content-Type', /json/)
@@ -155,7 +155,7 @@ describe('Phase Products', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/99999/products/${productId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
         })
         .send(updateBody)
         .expect('Content-Type', /json/)
@@ -166,7 +166,7 @@ describe('Phase Products', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/99999`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
         })
         .send(updateBody)
         .expect('Content-Type', /json/)
@@ -177,7 +177,7 @@ describe('Phase Products', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/99999`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
         })
         .send({
           estimatedPrice: -15,
@@ -212,6 +212,68 @@ describe('Phase Products', () => {
         });
     });
 
+    it('should return 200 if requested by admin', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(updateBody)
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.copilot}`,
+          })
+          .send(updateBody)
+          .expect('Content-Type', /json/)
+          .expect(403)
+          .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -229,7 +291,7 @@ describe('Phase Products', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED when name updated', (done) => {
+      it('should send correct BUS API messages when name updated', (done) => {
         request(server)
           .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -245,18 +307,29 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ resource: RESOURCES.PHASE_PRODUCT })).should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ name: 'new name' })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  name: 'new name',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED when estimatedPrice updated', (done) => {
+      it('should send correct BUS API messages when estimatedPrice updated', (done) => {
         request(server)
           .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -272,18 +345,29 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ resource: RESOURCES.PHASE_PRODUCT })).should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ estimatedPrice: 123 })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  estimatedPrice: 123,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED when actualPrice updated', (done) => {
+      it('should send correct BUS API messages when actualPrice updated', (done) => {
         request(server)
           .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -299,18 +383,29 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ resource: RESOURCES.PHASE_PRODUCT })).should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ actualPrice: 123 })).should.be.true;
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  actualPrice: 123,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED when details updated', (done) => {
+      it('should send correct BUS API messages when details updated', (done) => {
         request(server)
           .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -326,18 +421,31 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ resource: RESOURCES.PHASE_PRODUCT })).should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ details: 'something' })).should.be.true;
+                createEventSpy.callCount.should.be.eql(3);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  details: 'something',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PRODUCT_SPECIFICATION_MODIFIED)
+                  .should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('should not send message BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED when type updated', (done) => {
+      it('should send correct BUS API messages when type updated', (done) => {
         request(server)
           .patch(`/v5/projects/${projectId}/phases/${phaseId}/products/${productId}`)
           .set({
@@ -353,11 +461,13 @@ describe('Phase Products', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ resource: RESOURCES.PHASE_PRODUCT })).should.be.true;
-                createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED,
-                  sinon.match({ type: 'another type' })).should.be.true;
+                createEventSpy.callCount.should.be.eql(1);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  type: 'another type',
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/phases/create.js b/src/routes/phases/create.js
index 59aaee7..5207163 100644
--- a/src/routes/phases/create.js
+++ b/src/routes/phases/create.js
@@ -5,7 +5,7 @@ import Sequelize from 'sequelize';
 
 import models from '../../models';
 import util from '../../util';
-import { EVENT, RESOURCES } from '../../constants';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES } from '../../constants';
 
 const permissions = require('tc-core-library-js').middleware.permissions;
 
@@ -13,6 +13,8 @@ const permissions = require('tc-core-library-js').middleware.permissions;
 const addProjectPhaseValidations = {
   body: Joi.object().keys({
     name: Joi.string().required(),
+    description: Joi.string().optional(),
+    requirements: Joi.string().optional(),
     status: Joi.string().required(),
     startDate: Joi.date().optional(),
     endDate: Joi.date().optional(),
@@ -139,7 +141,7 @@ module.exports = [
         // Send events to buses
         req.log.debug('Sending event to RabbitMQ bus for project phase %d', newProjectPhase.id);
         req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED,
-          newProjectPhase,
+          { added: newProjectPhase, route: TIMELINE_REFERENCES.PHASE },
           { correlationId: req.id },
         );
 
@@ -156,7 +158,12 @@ module.exports = [
             req,
             EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
             RESOURCES.PHASE,
+            _.assign(_.pick(phase.toJSON(), 'id', 'order', 'updatedBy', 'updatedAt')),
+            // Pass the same object as original phase even though, the order has changed.
+            // So far we don't use the order so it's ok. But in general, we should pass
+            // the original phases. <- TODO
             _.assign(_.pick(phase.toJSON(), 'id', 'order', 'updatedBy', 'updatedAt'))),
+            true, // don't send event to Notification Service as the main event here is updating one phase
         );
 
         res.status(201).json(newProjectPhase);
diff --git a/src/routes/phases/create.spec.js b/src/routes/phases/create.spec.js
index 693683c..94ee55c 100644
--- a/src/routes/phases/create.spec.js
+++ b/src/routes/phases/create.spec.js
@@ -2,19 +2,28 @@
 import _ from 'lodash';
 import chai from 'chai';
 import sinon from 'sinon';
+import config from 'config';
 import request from 'supertest';
 import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
+import messageService from '../../services/messageService';
+import RabbitMQService from '../../services/rabbitmq';
+import mockRabbitMQ from '../../tests/mockRabbitMQ';
 import {
-  BUS_API_EVENT, RESOURCES,
+  BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT,
 } from '../../constants';
 
 const should = chai.should();
 
+const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
+const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType');
+
 const body = {
   name: 'test project phase',
+  description: 'test project phase description',
+  requirements: 'test project phase requirements',
   status: 'active',
   startDate: '2018-05-15T00:00:00Z',
   endDate: '2018-05-15T12:00:00Z',
@@ -30,6 +39,8 @@ const body = {
 const validatePhase = (resJson, expectedPhase) => {
   should.exist(resJson);
   resJson.name.should.be.eql(expectedPhase.name);
+  resJson.description.should.be.eql(expectedPhase.description);
+  resJson.requirements.should.be.eql(expectedPhase.requirements);
   resJson.status.should.be.eql(expectedPhase.status);
   resJson.budget.should.be.eql(expectedPhase.budget);
   resJson.progress.should.be.eql(expectedPhase.progress);
@@ -38,6 +49,7 @@ const validatePhase = (resJson, expectedPhase) => {
 
 describe('Project Phases', () => {
   let projectId;
+  let projectName;
   const memberUser = {
     handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
     userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
@@ -52,44 +64,44 @@ describe('Project Phases', () => {
     lastName: 'lName',
     email: 'some@abc.com',
   };
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
   let productTemplateId;
-  before((done) => {
+  beforeEach((done) => {
     // mocks
     testUtil.clearDb()
-      .then(() => {
-        models.Project.create({
-          type: 'generic',
-          billingAccountId: 1,
-          name: 'test1',
-          description: 'test project1',
-          status: 'draft',
-          details: {},
+      .then(() => models.Project.create(project).then((p) => {
+        projectId = p.id;
+        projectName = p.name;
+        // create members
+        return models.ProjectMember.bulkCreate([{
+          id: 1,
+          userId: copilotUser.userId,
+          projectId,
+          role: 'copilot',
+          isPrimary: false,
           createdBy: 1,
           updatedBy: 1,
-          lastActivityAt: 1,
-          lastActivityUserId: '1',
-        }).then((p) => {
-          projectId = p.id;
-          // create members
-          models.ProjectMember.bulkCreate([{
-            id: 1,
-            userId: copilotUser.userId,
-            projectId,
-            role: 'copilot',
-            isPrimary: false,
-            createdBy: 1,
-            updatedBy: 1,
-          }, {
-            id: 2,
-            userId: memberUser.userId,
-            projectId,
-            role: 'customer',
-            isPrimary: true,
-            createdBy: 1,
-            updatedBy: 1,
-          }]);
-        });
-      })
+        }, {
+          id: 2,
+          userId: memberUser.userId,
+          projectId,
+          role: 'customer',
+          isPrimary: true,
+          createdBy: 1,
+          updatedBy: 1,
+        }]);
+      }))
       .then(() =>
         models.ProductTemplate.create({
           name: 'name 1',
@@ -126,7 +138,7 @@ describe('Project Phases', () => {
       .then(() => done());
   });
 
-  after((done) => {
+  afterEach((done) => {
     testUtil.clearDb(done);
   });
 
@@ -222,7 +234,7 @@ describe('Project Phases', () => {
       request(server)
         .post('/v5/projects/99999/phases/')
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send(body)
         .expect('Content-Type', /json/)
@@ -345,6 +357,68 @@ describe('Project Phases', () => {
         });
     });
 
+    it('should return 201 if requested by admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/phases/`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end(done);
+    });
+
+    it('should return 201 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectId}/phases/`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(body)
+          .expect('Content-Type', /json/)
+          .expect(201)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/phases/`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectId}/phases/`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.copilot}`,
+          })
+          .send(body)
+          .expect('Content-Type', /json/)
+          .expect(403)
+          .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -362,7 +436,7 @@ describe('Project Phases', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_ADDED when phase added', (done) => {
+      it('should send correct BUS API messages when phase added', (done) => {
         request(server)
         .post(`/v5/projects/${projectId}/phases/`)
         .set({
@@ -376,24 +450,111 @@ describe('Project Phases', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ resource: RESOURCES.PHASE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ name: body.name })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ status: body.status })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ budget: body.budget })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ progress: body.progress })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED,
-                sinon.match({ projectId })).should.be.true;
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                name: body.name,
+                status: body.status,
+                budget: body.budget,
+                progress: body.progress,
+                projectId,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051332,
+                initiatorUserId: 40051332,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
     });
+
+    describe('RabbitMQ Message topic', () => {
+      let createMessageSpy;
+      let publishSpy;
+      let sandbox;
+
+      // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+      before(async () => new Promise(resolve => setTimeout(() => resolve(), 500)));
+
+      beforeEach(async () => {
+        sandbox = sinon.sandbox.create();
+        server.services.pubsub = new RabbitMQService(server.logger);
+
+        // initialize RabbitMQ
+        server.services.pubsub.init(
+          config.get('rabbitmqURL'),
+          config.get('pubsubExchangeName'),
+          config.get('pubsubQueueName'),
+        );
+
+        // add project to ES index
+        await server.services.es.index({
+          index: ES_PROJECT_INDEX,
+          type: ES_PROJECT_TYPE,
+          id: projectId,
+          body: {
+            doc: project,
+          },
+        });
+
+        return new Promise(resolve => setTimeout(() => {
+          publishSpy = sandbox.spy(server.services.pubsub, 'publish');
+          createMessageSpy = sandbox.spy(messageService, 'createTopic');
+          resolve();
+        }, 500));
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      after(() => {
+        mockRabbitMQ(server);
+      });
+
+      it('should send message topic when phase added', (done) => {
+        const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+          post: () => Promise.resolve({
+            status: 200,
+            data: {},
+          }),
+        });
+        sandbox.stub(messageService, 'getClient', () => mockHttpClient);
+        request(server)
+            .post(`/v5/projects/${projectId}/phases/`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.copilot}`,
+            })
+            .send(body)
+            .expect('Content-Type', /json/)
+            .expect(201)
+            .end((err) => {
+              if (err) {
+                done(err);
+              } else {
+                testUtil.wait(() => {
+                  publishSpy.calledOnce.should.be.true;
+                  publishSpy.calledWith('project.phase.added').should.be.true;
+                  createMessageSpy.calledOnce.should.be.true;
+                  createMessageSpy.calledWith(sinon.match({ reference: 'project',
+                    referenceId: '1',
+                    tag: 'phase#1',
+                    title: 'test project phase',
+                  })).should.be.true;
+                  done();
+                });
+              }
+            });
+      });
+    });
   });
 });
diff --git a/src/routes/phases/delete.js b/src/routes/phases/delete.js
index d035cc9..a7e6d2a 100644
--- a/src/routes/phases/delete.js
+++ b/src/routes/phases/delete.js
@@ -4,7 +4,7 @@ import _ from 'lodash';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import models from '../../models';
 import util from '../../util';
-import { EVENT, RESOURCES } from '../../constants';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES } from '../../constants';
 
 const permissions = tcMiddleware.permissions;
 
@@ -41,7 +41,7 @@ module.exports = [
         // Send events to buses
         req.app.services.pubsub.publish(
           EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED,
-          deleted,
+          { deleted, route: TIMELINE_REFERENCES.PHASE },
           { correlationId: req.id },
         );
 
diff --git a/src/routes/phases/delete.spec.js b/src/routes/phases/delete.spec.js
index 7bf7e1e..17eea96 100644
--- a/src/routes/phases/delete.spec.js
+++ b/src/routes/phases/delete.spec.js
@@ -3,18 +3,25 @@ import _ from 'lodash';
 import request from 'supertest';
 import sinon from 'sinon';
 import chai from 'chai';
+import config from 'config';
 import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-
+import messageService from '../../services/messageService';
+import RabbitMQService from '../../services/rabbitmq';
+import mockRabbitMQ from '../../tests/mockRabbitMQ';
 import {
   BUS_API_EVENT,
   RESOURCES,
+  CONNECT_NOTIFICATION_EVENT,
 } from '../../constants';
 
 const should = chai.should(); // eslint-disable-line no-unused-vars
 
+const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
+const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType');
+
 const expectAfterDelete = (projectId, id, err, next) => {
   if (err) throw err;
   setTimeout(() =>
@@ -52,6 +59,7 @@ const body = {
 describe('Project Phases', () => {
   let projectId;
   let phaseId;
+  let projectName;
   const memberUser = {
     handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
     userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
@@ -66,23 +74,34 @@ describe('Project Phases', () => {
     lastName: 'lName',
     email: 'some@abc.com',
   };
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
+  const topic = {
+    id: 1,
+    title: 'test project phase',
+    posts:
+    [{ id: 1,
+      type: 'post',
+      body: 'body',
+    }],
+  };
   beforeEach((done) => {
     // mocks
     testUtil.clearDb()
         .then(() => {
-          models.Project.create({
-            type: 'generic',
-            billingAccountId: 1,
-            name: 'test1',
-            description: 'test project1',
-            status: 'draft',
-            details: {},
-            createdBy: 1,
-            updatedBy: 1,
-            lastActivityAt: 1,
-            lastActivityUserId: '1',
-          }).then((p) => {
+          models.Project.create(project).then((p) => {
             projectId = p.id;
+            projectName = p.name;
             // create members
             models.ProjectMember.bulkCreate([{
               id: 1,
@@ -140,7 +159,7 @@ describe('Project Phases', () => {
       request(server)
         .delete(`/v5/projects/999/phases/${phaseId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .expect('Content-Type', /json/)
         .expect(404, done);
@@ -150,7 +169,7 @@ describe('Project Phases', () => {
       request(server)
         .delete(`/v5/projects/${projectId}/phases/999`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .expect('Content-Type', /json/)
         .expect(404, done);
@@ -162,9 +181,64 @@ describe('Project Phases', () => {
         .set({
           Authorization: `Bearer ${testUtil.jwts.copilot}`,
         })
+        .expect(204)
         .end(err => expectAfterDelete(projectId, phaseId, err, done));
     });
 
+    it('should return 204 if requested by admin', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(204)
+        .end(done);
+    });
+
+    it('should return 204 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .expect(204)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+          .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.copilot}`,
+          })
+          .expect(403)
+          .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
@@ -182,7 +256,7 @@ describe('Project Phases', () => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_DELETED when phase removed', (done) => {
+      it('should send correct BUS API messages when phase removed', (done) => {
         request(server)
         .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
         .set({
@@ -194,16 +268,102 @@ describe('Project Phases', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED,
-                sinon.match({ resource: RESOURCES.PHASE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED,
-                sinon.match({ id: phaseId })).should.be.true;
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051332,
+                initiatorUserId: 40051332,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
     });
+
+    describe('RabbitMQ Message topic', () => {
+      let deleteTopicSpy;
+      let deletePostsSpy;
+      let publishSpy;
+      let sandbox;
+
+      // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+      before(async () => new Promise(resolve => setTimeout(() => resolve(), 500)));
+
+      beforeEach(async () => {
+        sandbox = sinon.sandbox.create();
+        server.services.pubsub = new RabbitMQService(server.logger);
+
+        // initialize RabbitMQ
+        server.services.pubsub.init(
+          config.get('rabbitmqURL'),
+          config.get('pubsubExchangeName'),
+          config.get('pubsubQueueName'),
+        );
+
+        // add project to ES index
+        await server.services.es.index({
+          index: ES_PROJECT_INDEX,
+          type: ES_PROJECT_TYPE,
+          id: projectId,
+          body: {
+            doc: _.assign(project, { phases: [_.assign(body, { id: phaseId, projectId })] }),
+          },
+        });
+
+        return new Promise(resolve => setTimeout(() => {
+          publishSpy = sandbox.spy(server.services.pubsub, 'publish');
+          deleteTopicSpy = sandbox.spy(messageService, 'deleteTopic');
+          deletePostsSpy = sandbox.spy(messageService, 'deletePosts');
+          sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic));
+          resolve();
+        }, 500));
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      after(() => {
+        mockRabbitMQ(server);
+      });
+
+      it('should send message topic when phase deleted', (done) => {
+        const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+          delete: () => Promise.resolve(true),
+        });
+        sandbox.stub(messageService, 'getClient', () => mockHttpClient);
+        request(server)
+            .delete(`/v5/projects/${projectId}/phases/${phaseId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(204)
+            .end((err) => {
+              if (err) {
+                done(err);
+              } else {
+                testUtil.wait(() => {
+                  publishSpy.calledOnce.should.be.true;
+                  publishSpy.firstCall.calledWith('project.phase.removed').should.be.true;
+                  deleteTopicSpy.calledOnce.should.be.true;
+                  deleteTopicSpy.calledWith(topic.id).should.be.true;
+                  deletePostsSpy.calledWith(topic.id).should.be.true;
+                  done();
+                });
+              }
+            });
+      });
+    });
   });
 });
diff --git a/src/routes/phases/list.js b/src/routes/phases/list.js
index 04c6a63..5370c2e 100644
--- a/src/routes/phases/list.js
+++ b/src/routes/phases/list.js
@@ -64,7 +64,7 @@ module.exports = [
               if (!project) {
                 const apiErr = new Error(`active project not found for project id ${projectId}`);
                 apiErr.status = 404;
-                next(apiErr);
+                return next(apiErr);
               }
 
               // Get the phases
diff --git a/src/routes/phases/list.spec.js b/src/routes/phases/list.spec.js
index 1fc977c..d4d93a7 100644
--- a/src/routes/phases/list.spec.js
+++ b/src/routes/phases/list.spec.js
@@ -2,7 +2,7 @@
 import _ from 'lodash';
 import request from 'supertest';
 import config from 'config';
-import sleep from 'sleep';
+// import sleep from 'sleep';
 import chai from 'chai';
 import server from '../../app';
 import models from '../../models';
@@ -95,7 +95,7 @@ describe('Project Phases', () => {
               body: project,
             }).then(() => {
               // sleep for some time, let elasticsearch indices be settled
-              sleep.sleep(5);
+              // sleep.sleep(5);
               done();
             });
           });
diff --git a/src/routes/phases/update.js b/src/routes/phases/update.js
index bf85ffb..7bafa01 100644
--- a/src/routes/phases/update.js
+++ b/src/routes/phases/update.js
@@ -6,7 +6,7 @@ import Sequelize from 'sequelize';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import models from '../../models';
 import util from '../../util';
-import { EVENT, RESOURCES } from '../../constants';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES, ROUTES } from '../../constants';
 
 
 const permissions = tcMiddleware.permissions;
@@ -14,6 +14,8 @@ const permissions = tcMiddleware.permissions;
 const updateProjectPhaseValidation = {
   body: Joi.object().keys({
     name: Joi.string().optional(),
+    description: Joi.string().optional(),
+    requirements: Joi.string().optional(),
     status: Joi.string().optional(),
     startDate: Joi.date().optional(),
     endDate: Joi.date().optional(),
@@ -149,10 +151,12 @@ module.exports = [
       .then((allPhases) => {
         req.log.debug('updated project phase', JSON.stringify(updated, null, 2));
 
+        const updatedValue = updated.get({ plain: true });
+
         // emit original and updated project phase information
         req.app.services.pubsub.publish(
           EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
-          { original: previousValue, updated, allPhases },
+          { original: previousValue, updated: updatedValue, allPhases, route: TIMELINE_REFERENCES.PHASE },
           { correlationId: req.id },
         );
 
@@ -161,7 +165,9 @@ module.exports = [
           req,
           EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
           RESOURCES.PHASE,
-          _.assign(updatedProps, _.pick(updated, 'id', 'updatedAt')));
+          updatedValue,
+          previousValue,
+          ROUTES.PHASES.UPDATE);
 
         res.json(updated);
       })
diff --git a/src/routes/phases/update.spec.js b/src/routes/phases/update.spec.js
index bb68211..408670a 100644
--- a/src/routes/phases/update.spec.js
+++ b/src/routes/phases/update.spec.js
@@ -2,21 +2,30 @@
 import _ from 'lodash';
 import sinon from 'sinon';
 import chai from 'chai';
+import config from 'config';
 import request from 'supertest';
 import server from '../../app';
 import models from '../../models';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-
+import messageService from '../../services/messageService';
+import RabbitMQService from '../../services/rabbitmq';
+import mockRabbitMQ from '../../tests/mockRabbitMQ';
 import {
   BUS_API_EVENT,
   RESOURCES,
+  CONNECT_NOTIFICATION_EVENT,
 } from '../../constants';
 
+const ES_PROJECT_INDEX = config.get('elasticsearchConfig.indexName');
+const ES_PROJECT_TYPE = config.get('elasticsearchConfig.docType');
+
 const should = chai.should();
 
 const body = {
   name: 'test project phase',
+  description: 'test project phase description',
+  requirements: 'test project phase requirements',
   status: 'active',
   startDate: '2018-05-15T00:00:00Z',
   endDate: '2018-05-15T12:00:00Z',
@@ -31,6 +40,8 @@ const body = {
 
 const updateBody = {
   name: 'test project phase xxx',
+  description: 'test project phase description xxx',
+  requirements: 'test project phase requirements xxx',
   status: 'inactive',
   startDate: '2018-05-11T00:00:00Z',
   endDate: '2018-05-12T12:00:00Z',
@@ -44,6 +55,8 @@ const updateBody = {
 const validatePhase = (resJson, expectedPhase) => {
   should.exist(resJson);
   resJson.name.should.be.eql(expectedPhase.name);
+  resJson.description.should.be.eql(expectedPhase.description);
+  resJson.requirements.should.be.eql(expectedPhase.requirements);
   resJson.status.should.be.eql(expectedPhase.status);
   resJson.budget.should.be.eql(expectedPhase.budget);
   resJson.progress.should.be.eql(expectedPhase.progress);
@@ -52,8 +65,10 @@ const validatePhase = (resJson, expectedPhase) => {
 
 describe('Project Phases', () => {
   let projectId;
+  let projectName;
   let phaseId;
   let phaseId2;
+  let phaseId3;
   const memberUser = {
     handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
     userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
@@ -68,23 +83,34 @@ describe('Project Phases', () => {
     lastName: 'lName',
     email: 'some@abc.com',
   };
-  before((done) => {
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
+  const topic = {
+    id: 1,
+    title: 'test project phase',
+    posts:
+    [{ id: 1,
+      type: 'post',
+      body: 'body',
+    }],
+  };
+  beforeEach((done) => {
     // mocks
     testUtil.clearDb()
       .then(() => {
-        models.Project.create({
-          type: 'generic',
-          billingAccountId: 1,
-          name: 'test1',
-          description: 'test project1',
-          status: 'draft',
-          details: {},
-          createdBy: 1,
-          updatedBy: 1,
-          lastActivityAt: 1,
-          lastActivityUserId: '1',
-        }).then((p) => {
+        models.Project.create(project).then((p) => {
           projectId = p.id;
+          projectName = p.name;
           // create members
           models.ProjectMember.bulkCreate([{
             id: 1,
@@ -107,11 +133,13 @@ describe('Project Phases', () => {
             const phases = [
               body,
               _.assign({ order: 1 }, body),
+              _.assign({}, body, { status: 'draft' }),
             ];
             models.ProjectPhase.bulkCreate(phases, { returning: true })
               .then((createdPhases) => {
                 phaseId = createdPhases[0].id;
                 phaseId2 = createdPhases[1].id;
+                phaseId3 = createdPhases[2].id;
 
                 done();
               });
@@ -120,7 +148,7 @@ describe('Project Phases', () => {
       });
   });
 
-  after((done) => {
+  afterEach((done) => {
     testUtil.clearDb(done);
   });
 
@@ -151,7 +179,7 @@ describe('Project Phases', () => {
       request(server)
         .patch(`/v5/projects/999/phases/${phaseId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send(updateBody)
         .expect('Content-Type', /json/)
@@ -162,7 +190,7 @@ describe('Project Phases', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/999`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send(updateBody)
         .expect('Content-Type', /json/)
@@ -173,7 +201,7 @@ describe('Project Phases', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send({
           progress: -15,
@@ -186,7 +214,7 @@ describe('Project Phases', () => {
       request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
         .set({
-          Authorization: `Bearer ${testUtil.jwts.manager}`,
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send({
           endDate: '2018-05-13T00:00:00Z',
@@ -267,24 +295,286 @@ describe('Project Phases', () => {
         });
     });
 
+    it('should return 200 if requested by admin', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(_.assign({ order: 1 }, updateBody))
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 if requested by manager which is a member', (done) => {
+      models.ProjectMember.create({
+        id: 3,
+        userId: testUtil.userIds.manager,
+        projectId,
+        role: 'manager',
+        isPrimary: false,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(_.assign({ order: 1 }, updateBody))
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end(done);
+      });
+    });
+
+    it('should return 403 if requested by manager which is not a member', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(_.assign({ order: 1 }, updateBody))
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if requested by non-member copilot', (done) => {
+      models.ProjectMember.destroy({
+        where: { userId: testUtil.userIds.copilot, projectId },
+      }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.copilot}`,
+          })
+          .send(_.assign({ order: 1 }, updateBody))
+          .expect('Content-Type', /json/)
+          .expect(403)
+          .end(done);
+      });
+    });
+
     describe('Bus api', () => {
       let createEventSpy;
       const sandbox = sinon.sandbox.create();
 
+
       before((done) => {
         // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
         testUtil.wait(done);
       });
 
+
       beforeEach(() => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
+
       afterEach(() => {
         sandbox.restore();
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_UPDATED when startDate updated', (done) => {
+      it('should send correct BUS API messages when spentBudget updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          spentBudget: 123,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_PAYMENT).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when progress updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          progress: 50,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_PROGRESS).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PROGRESS_MODIFIED).should.be.true;
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when details updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          details: {
+            text: 'something',
+          },
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_UPDATE_SCOPE).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when status updated (completed)', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          status: 'completed',
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_TRANSITION_COMPLETED).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when status updated (active)', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId3}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          status: 'active',
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId3,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PHASE_TRANSITION_ACTIVE).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when budget updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          budget: 123,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when startDate updated', (done) => {
         request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
         .set({
@@ -300,20 +590,31 @@ describe('Project Phases', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              // createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED,
-                sinon.match({ resource: RESOURCES.PHASE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED,
-                sinon.match({ id: phaseId })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED,
-                sinon.match({ updatedBy: testUtil.userIds.copilot })).should.be.true;
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051332,
+                initiatorUserId: 40051332,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('should send message BUS_API_EVENT.PROJECT_PHASE_UPDATED when duration updated', (done) => {
+
+      it('should send correct BUS API messages when duration updated', (done) => {
         request(server)
         .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
         .set({
@@ -329,14 +630,168 @@ describe('Project Phases', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED,
-                sinon.match({ duration: 100 })).should.be.true;
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                duration: 100,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051332,
+                initiatorUserId: 40051332,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when order updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          order: 100,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              // NOTE: no other event should be called, as this phase doesn't move any other phases
+
               done();
             });
           }
         });
       });
+
+      it('should send correct BUS API messages when endDate updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          endDate: new Date(),
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: phaseId,
+                updatedBy: testUtil.userIds.copilot,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+
+    describe('RabbitMQ Message topic', () => {
+      let updateMessageSpy;
+      let publishSpy;
+      let sandbox;
+
+      // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+      before(async () => new Promise(resolve => setTimeout(() => resolve(), 500)));
+
+      beforeEach(async () => {
+        sandbox = sinon.sandbox.create();
+        server.services.pubsub = new RabbitMQService(server.logger);
+
+        // initialize RabbitMQ
+        server.services.pubsub.init(
+          config.get('rabbitmqURL'),
+          config.get('pubsubExchangeName'),
+          config.get('pubsubQueueName'),
+        );
+
+        // add project to ES index
+        await server.services.es.index({
+          index: ES_PROJECT_INDEX,
+          type: ES_PROJECT_TYPE,
+          id: projectId,
+          body: {
+            doc: _.assign(project, { phases: [_.assign(body, { id: phaseId, projectId })] }),
+          },
+        });
+
+        return new Promise(resolve => setTimeout(() => {
+          publishSpy = sandbox.spy(server.services.pubsub, 'publish');
+          updateMessageSpy = sandbox.spy(messageService, 'updateTopic');
+          sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic));
+          resolve();
+        }, 500));
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      after(() => {
+        mockRabbitMQ(server);
+      });
+
+      it('should send message topic when phase Updated', (done) => {
+        const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+          post: () => Promise.resolve({
+            status: 200,
+            data: {},
+          }),
+        });
+        sandbox.stub(messageService, 'getClient', () => mockHttpClient);
+        request(server)
+            .patch(`/v5/projects/${projectId}/phases/${phaseId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(_.assign(updateBody, { budget: 123 }))
+            .expect('Content-Type', /json/)
+            .expect(200)
+            .end((err) => {
+              if (err) {
+                done(err);
+              } else {
+                testUtil.wait(() => {
+                  publishSpy.calledOnce.should.be.true;
+                  publishSpy.calledWith('project.phase.updated').should.be.true;
+                  updateMessageSpy.calledOnce.should.be.true;
+                  updateMessageSpy.calledWith(topic.id, sinon.match({
+                    title: updateBody.name,
+                    postId: topic.posts[0].id,
+                    content: topic.posts[0].body })).should.be.true;
+                  done();
+                });
+              }
+            });
+      });
     });
   });
 });
diff --git a/src/routes/planConfig/version/create.spec.js b/src/routes/planConfig/version/create.spec.js
index 1b564d7..d3c1956 100644
--- a/src/routes/planConfig/version/create.spec.js
+++ b/src/routes/planConfig/version/create.spec.js
@@ -55,7 +55,8 @@ describe('CREATE PlanConfig version', () => {
       request(server)
         .post('/v5/projects/metadata/planConfig/dev/versions')
         .send(body)
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 400 if missing config', (done) => {
@@ -70,7 +71,8 @@ describe('CREATE PlanConfig version', () => {
       })
         .send(invalidBody)
         .expect('Content-Type', /json/)
-        .expect(400, done);
+        .expect(400)
+        .end(done);
     });
 
     it('should return 201 for admin', (done) => {
@@ -106,7 +108,8 @@ describe('CREATE PlanConfig version', () => {
         Authorization: `Bearer ${testUtil.jwts.member}`,
       })
         .send(body)
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
   });
 });
diff --git a/src/routes/planConfig/version/delete.spec.js b/src/routes/planConfig/version/delete.spec.js
index a47b509..79097e6 100644
--- a/src/routes/planConfig/version/delete.spec.js
+++ b/src/routes/planConfig/version/delete.spec.js
@@ -70,7 +70,8 @@ describe('DELETE planConfig version', () => {
     it('should return 403 if user is not authenticated', (done) => {
       request(server)
         .delete('/v5/projects/metadata/planConfig/dev/versions/1')
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 403 for member', (done) => {
@@ -79,7 +80,8 @@ describe('DELETE planConfig version', () => {
       .set({
         Authorization: `Bearer ${testUtil.jwts.member}`,
       })
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 403 for copilot', (done) => {
@@ -88,7 +90,8 @@ describe('DELETE planConfig version', () => {
       .set({
         Authorization: `Bearer ${testUtil.jwts.copilot}`,
       })
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 403 for manager', (done) => {
@@ -97,7 +100,8 @@ describe('DELETE planConfig version', () => {
       .set({
         Authorization: `Bearer ${testUtil.jwts.manager}`,
       })
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 404 for non-existed key', (done) => {
@@ -106,7 +110,8 @@ describe('DELETE planConfig version', () => {
         .set({
           Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
-        .expect(404, done);
+        .expect(404)
+        .end(done);
     });
 
     it('should return 404 for non-existed version', (done) => {
@@ -115,7 +120,8 @@ describe('DELETE planConfig version', () => {
         .set({
           Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
-        .expect(404, done);
+        .expect(404)
+        .end(done);
     });
 
     it('should return 204, for admin', (done) => {
diff --git a/src/routes/planConfig/version/get.spec.js b/src/routes/planConfig/version/get.spec.js
index d608a76..a745579 100644
--- a/src/routes/planConfig/version/get.spec.js
+++ b/src/routes/planConfig/version/get.spec.js
@@ -92,7 +92,8 @@ describe('GET a latest version of specific key of PlanConfig', () => {
     it('should return 403 if user is not authenticated', (done) => {
       request(server)
       .get('/v5/projects/metadata/planConfig/dev')
-      .expect(403, done);
+      .expect(403)
+      .end(done);
     });
 
     it('should return 200 for connect admin', (done) => {
@@ -121,7 +122,8 @@ describe('GET a latest version of specific key of PlanConfig', () => {
       .set({
         Authorization: `Bearer ${testUtil.jwts.member}`,
       })
-        .expect(200, done);
+        .expect(200)
+        .end(done);
     });
 
     it('should return 200 for copilot', (done) => {
@@ -130,7 +132,8 @@ describe('GET a latest version of specific key of PlanConfig', () => {
       .set({
         Authorization: `Bearer ${testUtil.jwts.copilot}`,
       })
-        .expect(200, done);
+        .expect(200)
+        .end(done);
     });
   });
 });
diff --git a/src/routes/planConfig/version/getVersion.js b/src/routes/planConfig/version/getVersion.js
index ae2d722..7673c3c 100644
--- a/src/routes/planConfig/version/getVersion.js
+++ b/src/routes/planConfig/version/getVersion.js
@@ -44,15 +44,7 @@ module.exports = [
   .then((data) => {
     if (data.length === 0) {
       req.log.debug('No planConfig found in ES');
-      models.PlanConfig.findOne({
-        where: {
-          key: req.params.key,
-          version: req.params.version,
-        },
-        order: [['revision', 'DESC']],
-        limit: 1,
-        attributes: { exclude: ['deletedAt', 'deletedBy'] },
-      })
+      return models.PlanConfig.findOneWithLatestRevision(req.params)
         .then((planConfig) => {
           // Not found
           if (!planConfig) {
@@ -64,10 +56,10 @@ module.exports = [
           return Promise.resolve();
         })
         .catch(next);
-    } else {
-      req.log.debug('planConfigs found in ES');
-      res.json(data[0].inner_hits.planConfigs.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
     }
+    req.log.debug('planConfigs found in ES');
+    res.json(data[0].inner_hits.planConfigs.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
+    return Promise.resolve();
   })
   .catch(next),
 ];
diff --git a/src/routes/planConfig/version/update.spec.js b/src/routes/planConfig/version/update.spec.js
index b59283c..025b2b0 100644
--- a/src/routes/planConfig/version/update.spec.js
+++ b/src/routes/planConfig/version/update.spec.js
@@ -55,7 +55,8 @@ describe('UPDATE PlanConfig version', () => {
       request(server)
         .patch('/v5/projects/metadata/planConfig/dev/versions/1')
         .send(body)
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 400 if missing config', (done) => {
@@ -69,7 +70,8 @@ describe('UPDATE PlanConfig version', () => {
       })
         .send(invalidBody)
         .expect('Content-Type', /json/)
-        .expect(400, done);
+        .expect(400)
+        .end(done);
     });
 
     it('should return 201 for admin', (done) => {
@@ -105,7 +107,8 @@ describe('UPDATE PlanConfig version', () => {
         Authorization: `Bearer ${testUtil.jwts.member}`,
       })
         .send(body)
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
   });
 });
diff --git a/src/routes/priceConfig/version/getVersion.js b/src/routes/priceConfig/version/getVersion.js
index c0593fd..c6b9120 100644
--- a/src/routes/priceConfig/version/getVersion.js
+++ b/src/routes/priceConfig/version/getVersion.js
@@ -44,15 +44,7 @@ module.exports = [
     .then((data) => {
       if (data.length === 0) {
         req.log.debug('No priceConfig found in ES');
-        models.PriceConfig.findOne({
-          where: {
-            key: req.params.key,
-            version: req.params.version,
-          },
-          order: [['revision', 'DESC']],
-          limit: 1,
-          attributes: { exclude: ['deletedAt', 'deletedBy'] },
-        })
+        return models.PriceConfig.findOneWithLatestRevision(req.params)
           .then((priceConfig) => {
             // Not found
             if (!priceConfig) {
@@ -64,10 +56,10 @@ module.exports = [
             return Promise.resolve();
           })
           .catch(next);
-      } else {
-        req.log.debug('priceConfigs found in ES');
-        res.json(data[0].inner_hits.priceConfigs.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
       }
+      req.log.debug('priceConfigs found in ES');
+      res.json(data[0].inner_hits.priceConfigs.hits.hits[0]._source); // eslint-disable-line no-underscore-dangle
+      return Promise.resolve();
     })
     .catch(next);
   },
diff --git a/src/routes/productCategories/create.js b/src/routes/productCategories/create.js
index 65c9413..4abce16 100644
--- a/src/routes/productCategories/create.js
+++ b/src/routes/productCategories/create.js
@@ -40,10 +40,10 @@ module.exports = [
     });
 
     // Check if duplicated key
-    return models.ProductCategory.findByPk(req.body.key)
+    return models.ProductCategory.findByPk(req.body.key, { paranoid: false })
       .then((existing) => {
         if (existing) {
-          const apiErr = new Error(`Product category already exists for key ${req.params.key}`);
+          const apiErr = new Error(`Product category already exists (may be deleted) for key ${req.body.key}`);
           apiErr.status = 400;
           return Promise.reject(apiErr);
         }
diff --git a/src/routes/productCategories/list.spec.js b/src/routes/productCategories/list.spec.js
index 8bb216e..9dcbc07 100644
--- a/src/routes/productCategories/list.spec.js
+++ b/src/routes/productCategories/list.spec.js
@@ -37,7 +37,9 @@ describe('LIST product categories', () => {
       updatedBy: 1,
     },
   ];
-
+  before((done) => {
+    testUtil.clearES(done);
+  });
   beforeEach((done) => {
     testUtil.clearDb()
       .then(() => models.ProductCategory.create(productCategories[0]))
diff --git a/src/routes/productTemplates/list.js b/src/routes/productTemplates/list.js
index 6817fb5..f1816d8 100644
--- a/src/routes/productTemplates/list.js
+++ b/src/routes/productTemplates/list.js
@@ -16,7 +16,7 @@ module.exports = [
       if (!util.isValidFilter(filters, ['productKey'])) {
         util.handleError('Invalid filters', null, req, next);
       }
-      const where = { deletedAt: { $eq: null } };
+      const where = { deletedAt: { $eq: null }, disabled: false };
       if (filters.productKey) {
         where.productKey = { $eq: filters.productKey };
       }
diff --git a/src/routes/productTemplates/list.spec.js b/src/routes/productTemplates/list.spec.js
index 50d6ff7..09af469 100644
--- a/src/routes/productTemplates/list.spec.js
+++ b/src/routes/productTemplates/list.spec.js
@@ -9,11 +9,11 @@ import models from '../../models';
 import server from '../../app';
 import testUtil from '../../tests/util';
 
-const should = chai.should();
+const should = chai.should(); // eslint-disable-line no-unused-vars
 
 const validateProductTemplates = (count, resJson, expectedTemplates) => {
-  should.exist(resJson);
-  resJson.length.should.be.eql(count);
+  resJson.should.have.length(count);
+
   resJson.forEach((pt, idx) => {
     pt.should.include.all.keys('id', 'name', 'productKey', 'category', 'subCategory', 'icon', 'brief', 'details',
     'aliases', 'template', 'disabled', 'form', 'hidden', 'isAddOn', 'createdBy', 'createdAt', 'updatedBy', 'updatedAt');
@@ -52,7 +52,7 @@ describe('LIST product templates', () => {
         },
         alias2: [1, 2, 3],
       },
-      disabled: true,
+      disabled: false,
       hidden: true,
       isAddOn: true,
       template: {
diff --git a/src/routes/productTemplates/upgrade.spec.js b/src/routes/productTemplates/upgrade.spec.js
index d367854..9d39c69 100644
--- a/src/routes/productTemplates/upgrade.spec.js
+++ b/src/routes/productTemplates/upgrade.spec.js
@@ -86,7 +86,7 @@ describe('UPGRADE product template', () => {
     ]))
     .then(() => {
       const config = {
-        questions: [{
+        sections: [{
           id: 'appDefinition',
           title: 'Sample Project',
           required: true,
diff --git a/src/routes/projectEstimationItems/list.js b/src/routes/projectEstimationItems/list.js
new file mode 100644
index 0000000..c958c75
--- /dev/null
+++ b/src/routes/projectEstimationItems/list.js
@@ -0,0 +1,49 @@
+/**
+ * API to get project estimation items
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    estimationId: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('projectEstimation.item.list'),
+  (req, res, next) => models.ProjectEstimation.findOne({
+    where: {
+      id: req.params.estimationId,
+      projectId: req.params.projectId,
+      deletedAt: { $eq: null },
+    },
+    raw: true,
+  }).then((estimation) => {
+    if (!estimation) {
+      const apiErr = new Error('Project Estimation not found for projectId ' +
+        `${req.params.projectId} and estimation id ${req.params.estimationId}`);
+      apiErr.status = 404;
+      return Promise.reject(apiErr);
+    }
+    return models.ProjectEstimationItem.findAll({
+      where: {
+        projectEstimationId: req.params.estimationId,
+        deletedAt: { $eq: null },
+      },
+      attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      raw: true,
+      reqUser: req.authUser,
+      members: req.context.currentProjectMembers,
+    }).then((items) => {
+      res.json(items);
+      return Promise.resolve();
+    });
+  }).catch(next),
+];
diff --git a/src/routes/projectEstimationItems/list.spec.js b/src/routes/projectEstimationItems/list.spec.js
new file mode 100644
index 0000000..6e3c444
--- /dev/null
+++ b/src/routes/projectEstimationItems/list.spec.js
@@ -0,0 +1,233 @@
+/**
+ * Tests for list.js
+ */
+import chai from 'chai';
+import request from 'supertest';
+import _ from 'lodash';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+const project = {
+  id: 1,
+  name: 'test project 1',
+  type: 'generic',
+  status: 'active',
+  createdBy: 1,
+  updatedBy: 1,
+  lastActivityAt: new Date(),
+  lastActivityUserId: 1,
+};
+
+const projectMembers = [
+  {
+    userId: testUtil.userIds.admin,
+    role: 'manager',
+    projectId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    userId: testUtil.userIds.copilot,
+    role: 'copilot',
+    projectId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    userId: testUtil.userIds.member,
+    role: 'manager',
+    projectId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+];
+
+const projectEstimations = [
+  {
+    id: 1,
+    buildingBlockKey: 'key1',
+    conditions: ' empty condition ',
+    price: 1000,
+    quantity: 100,
+    minTime: 2,
+    maxTime: 2,
+    metadata: {},
+    projectId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+];
+
+const projectEstimationItems = [
+  {
+    projectEstimationId: 1,
+    price: 1234,
+    type: 'community',
+    markupUsedReference: 'buildingBlock',
+    markupUsedReferenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    projectEstimationId: 1,
+    price: 5678,
+    type: 'topcoder_service',
+    markupUsedReference: 'buildingBlock',
+    markupUsedReferenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    projectEstimationId: 1,
+    price: 1982,
+    type: 'fee',
+    markupUsedReference: 'buildingBlock',
+    markupUsedReferenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+];
+
+describe('GET project estimation items', () => {
+  beforeEach(() => testUtil.clearDb()
+    .then(() => models.Project.create(project))
+    .then(() => models.ProjectMember.bulkCreate(projectMembers))
+    .then(() => models.ProjectEstimation.bulkCreate(projectEstimations))
+    .then(() => models.ProjectEstimationItem.bulkCreate(projectEstimationItems))
+    .then(() => Promise.resolve()),
+  );
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  const url = '/v5/projects/1/estimations/1/items';
+
+  describe(`GET ${url}`, () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(url)
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 403 if user is not copilot or above', (done) => {
+      request(server)
+        .get(url)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect(403)
+        .end(done);
+    });
+
+    it('should return 404 if project not exists', (done) => {
+      request(server)
+        .get('/v5/projects/999/estimations/1/items')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404)
+        .end(done);
+    });
+
+    it('should return 404 if project estimation not exists', (done) => {
+      request(server)
+        .get('/v5/projects/1/estimations/999/items')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404)
+        .end(done);
+    });
+
+    it('should return all project estimation items for admin', (done) => {
+      request(server)
+        .get(url)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.length.should.be.eql(3);
+            // convert items to map with type.
+            const itemMap = {};
+            _.forEach(resJson, (item) => {
+              itemMap[item.type] = item;
+            });
+            should.exist(itemMap.community);
+            itemMap.community.price.should.be.eql(1234);
+            itemMap.community.projectEstimationId.should.be.eql(1);
+            itemMap.community.type.should.be.eql('community');
+
+            should.exist(itemMap.topcoder_service);
+            itemMap.topcoder_service.price.should.be.eql(5678);
+            itemMap.topcoder_service.projectEstimationId.should.be.eql(1);
+            itemMap.topcoder_service.type.should.be.eql('topcoder_service');
+
+            should.exist(itemMap.fee);
+            itemMap.fee.price.should.be.eql(1982);
+            itemMap.fee.projectEstimationId.should.be.eql(1);
+            itemMap.fee.type.should.be.eql('fee');
+
+            done();
+          }
+        });
+    });
+
+    it('should return 1 project estimation item for copilot', (done) => {
+      request(server)
+        .get(url)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.length.should.be.eql(1);
+            // convert items to map with type.
+            should.exist(resJson[0]);
+
+            const item = resJson[0];
+
+            item.price.should.be.eql(1234);
+            item.projectEstimationId.should.be.eql(1);
+            item.type.should.be.eql('community');
+
+            done();
+          }
+        });
+    });
+
+    it('should return 0 project estimation items for a project manager who is not a topcoder manager', (done) => {
+      request(server)
+        .get(url)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.length.should.be.eql(0);
+            // convert items to map with type.
+            done();
+          }
+        });
+    });
+  });
+});
diff --git a/src/routes/projectMemberInvites/create.js b/src/routes/projectMemberInvites/create.js
index b9e68fc..e462044 100644
--- a/src/routes/projectMemberInvites/create.js
+++ b/src/routes/projectMemberInvites/create.js
@@ -8,7 +8,9 @@ import { middleware as tcMiddleware } from 'tc-core-library-js';
 import models from '../../models';
 import util from '../../util';
 import { PROJECT_MEMBER_ROLE, PROJECT_MEMBER_MANAGER_ROLES,
-  MANAGER_ROLES, INVITE_STATUS, EVENT, RESOURCES, USER_ROLE } from '../../constants';
+  MANAGER_ROLES, INVITE_STATUS, EVENT, RESOURCES, USER_ROLE,
+  MAX_PARALLEL_REQUEST_QTY, CONNECT_NOTIFICATION_EVENT } from '../../constants';
+import { createEvent } from '../../services/busApi';
 
 /**
  * API to create member invite to project.
@@ -56,14 +58,25 @@ const compareEmail = (email1, email2, options = { UNIQUE_GMAIL_VALIDATION: false
  * @param {Array}  invites existent invites from DB
  * @param {Object} data    template for new invites to be put in DB
  * @param {Array}  failed  failed invites error message
+ * @param {Array} members  already members of the project
  *
  * @returns {Promise<Promise[]>} list of promises
  */
-const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
+const buildCreateInvitePromises = (req, invite, invites, data, failed, members) => {
   const invitePromises = [];
   if (invite.userIds) {
     // remove invites for users that are invited already
-    _.remove(invite.userIds, u => _.some(invites, i => i.userId === u));
+    const errMessageForAlreadyInvitedUsers = 'User with such handle is already invited to this project.';
+    _.remove(invite.userIds, u => _.some(invites, (i) => {
+      const isPresent = i.userId === u;
+      if (isPresent) {
+        failed.push(_.assign({}, {
+          userId: u,
+          message: errMessageForAlreadyInvitedUsers,
+        }));
+      }
+      return isPresent;
+    }));
     invite.userIds.forEach((userId) => {
       const dataNew = _.clone(data);
 
@@ -76,7 +89,7 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
   if (invite.emails) {
     // if for some emails there are already existent users, we will invite them by userId,
     // to avoid sending them registration email
-    return util.lookupUserEmails(req, invite.emails)
+    return util.lookupMultipleUserEmails(req, invite.emails, MAX_PARALLEL_REQUEST_QTY)
       .then((existentUsers) => {
         // existent user we will invite by userId and email
         const existentUsersWithNumberId = existentUsers.map((user) => {
@@ -91,23 +104,63 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
           !_.find(existentUsers, existentUser =>
             compareEmail(existentUser.email, inviteEmail, { UNIQUE_GMAIL_VALIDATION: false })),
         );
+
+        // remove users that are already member of the team
+        const errMessageForAlreadyMemberUsers = 'User with such email is already a member of the team.';
+
+        _.remove(existentUsersWithNumberId, user => _.some(members, (m) => {
+          const isPresent = (m.userId === user.id);
+          if (isPresent) {
+            failed.push(_.assign({}, {
+              email: user.email,
+              message: errMessageForAlreadyMemberUsers,
+            }));
+          }
+          return isPresent;
+        }));
+
         // remove invites for users that are invited already
-        _.remove(existentUsersWithNumberId, user => _.some(invites, i => i.userId === user.id));
+        const errMessageForAlreadyInvitedUsers = 'User with such email is already invited to this project.';
+
+        _.remove(existentUsersWithNumberId, user => _.some(invites, (i) => {
+          const isPresent = (i.userId === user.id);
+          if (isPresent) {
+            failed.push(_.assign({}, {
+              email: i.email,
+              message: errMessageForAlreadyInvitedUsers,
+            }));
+          }
+          return isPresent;
+        }));
+
         existentUsersWithNumberId.forEach((user) => {
           const dataNew = _.clone(data);
 
           dataNew.userId = user.id;
-          dataNew.email = user.email;
+          dataNew.email = user.email ? user.email.toLowerCase() : user.email;
+
           invitePromises.push(models.ProjectMemberInvite.create(dataNew));
         });
         // remove invites for users that are invited already
         _.remove(nonExistentUserEmails, email =>
-          _.some(invites, i =>
-            compareEmail(i.email, email, { UNIQUE_GMAIL_VALIDATION: config.get('UNIQUE_GMAIL_VALIDATION') })));
+          _.some(invites, (i) => {
+            const areEmailsSame = compareEmail(i.email, email, {
+              UNIQUE_GMAIL_VALIDATION: config.get('UNIQUE_GMAIL_VALIDATION'),
+            });
+            if (areEmailsSame) {
+              failed.push(_.assign({}, {
+                email: i.email,
+                message: errMessageForAlreadyInvitedUsers,
+              }));
+            }
+            return areEmailsSame;
+          }),
+        );
         nonExistentUserEmails.forEach((email) => {
           const dataNew = _.clone(data);
 
-          dataNew.email = email;
+          dataNew.email = email.toLowerCase();
+
           invitePromises.push(models.ProjectMemberInvite.create(dataNew));
         });
         return invitePromises;
@@ -120,8 +173,9 @@ const buildCreateInvitePromises = (req, invite, invites, data, failed) => {
   return invitePromises;
 };
 
-const sendInviteEmail = (req, projectId) => {
+const sendInviteEmail = (req, projectId, invite) => {
   req.log.debug(req.authUser);
+  const emailEventType = CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED;
   const promises = [
     models.Project.findOne({
       where: { id: projectId },
@@ -131,6 +185,40 @@ const sendInviteEmail = (req, projectId) => {
   ];
   return Promise.all(promises).then((responses) => {
     req.log.debug(responses);
+    const project = responses[0];
+    const initiator = responses[1] && responses[1].length ? responses[1][0] : {
+      userId: req.authUser.userId,
+      firstName: 'Connect',
+      lastName: 'User',
+    };
+    createEvent(emailEventType, {
+      data: {
+        connectURL: config.get('connectUrl'),
+        accountsAppURL: config.get('accountsAppUrl'),
+        subject: config.get('inviteEmailSubject'),
+        projects: [{
+          name: project.name,
+          projectId,
+          sections: [
+            {
+              EMAIL_INVITES: true,
+              title: config.get('inviteEmailSectionTitle'),
+              projectName: project.name,
+              projectId,
+              initiator,
+              isSSO: util.isSSO(project),
+            },
+          ],
+        }],
+      },
+      recipients: [invite.email],
+      version: 'v3',
+      from: {
+        name: config.get('EMAIL_INVITE_FROM_NAME'),
+        email: config.get('EMAIL_INVITE_FROM_EMAIL'),
+      },
+      categories: [`${process.env.NODE_ENV}:${emailEventType}`.toLowerCase()],
+    }, req.log);
   }).catch((error) => {
     req.log.error(error);
   });
@@ -160,9 +248,19 @@ module.exports = [
     const projectId = _.parseInt(req.params.projectId);
 
     const promises = [];
+    const errorMessageForAlreadyMemberUser = 'User with such handle is already a member of the team.';
     if (invite.userIds) {
       // remove members already in the team
-      _.remove(invite.userIds, u => _.some(members, m => m.userId === u));
+      _.remove(invite.userIds, u => _.some(members, (m) => {
+        const isPresent = m.userId === u;
+        if (isPresent) {
+          failed.push(_.assign({}, {
+            userId: m.userId,
+            message: errorMessageForAlreadyMemberUser,
+          }));
+        }
+        return isPresent;
+      }));
         // permission:
         // user has to have constants.MANAGER_ROLES role
         // to be invited as PROJECT_MEMBER_ROLE.MANAGER
@@ -187,7 +285,7 @@ module.exports = [
     }
     return Promise.all(promises).then((rolesList) => {
       if (!!invite.userIds && _.includes(PROJECT_MEMBER_MANAGER_ROLES, invite.role)) {
-        req.log.debug('Chekcing if userId is allowed as manager');
+        req.log.debug('Checking if userId is allowed as manager');
         const forbidUserList = [];
         _.zip(invite.userIds, rolesList).forEach((data) => {
           const [userId, roles] = data;
@@ -218,7 +316,7 @@ module.exports = [
           };
 
           req.log.debug('Creating invites');
-          return models.Sequelize.Promise.all(buildCreateInvitePromises(req, invite, invites, data, failed))
+          return models.Sequelize.Promise.all(buildCreateInvitePromises(req, invite, invites, data, failed, members))
             .then((values) => {
               values.forEach((v) => {
                 // emit the event
@@ -235,7 +333,7 @@ module.exports = [
                     );
                 // send email invite (async)
                 if (v.email && !v.userId && v.status === INVITE_STATUS.PENDING) {
-                  sendInviteEmail(req, projectId);
+                  sendInviteEmail(req, projectId, v);
                 }
               });
               return values;
diff --git a/src/routes/projectMemberInvites/create.spec.js b/src/routes/projectMemberInvites/create.spec.js
index e57c04c..6aac5c0 100644
--- a/src/routes/projectMemberInvites/create.spec.js
+++ b/src/routes/projectMemberInvites/create.spec.js
@@ -9,7 +9,14 @@ import util from '../../util';
 import server from '../../app';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { USER_ROLE, PROJECT_MEMBER_ROLE, INVITE_STATUS, BUS_API_EVENT, RESOURCES } from '../../constants';
+import {
+  USER_ROLE,
+  PROJECT_MEMBER_ROLE,
+  INVITE_STATUS,
+  BUS_API_EVENT,
+  RESOURCES,
+  CONNECT_NOTIFICATION_EVENT,
+} from '../../constants';
 
 const should = chai.should();
 
@@ -51,6 +58,15 @@ describe('Project Member Invite create', () => {
             createdBy: 1,
             updatedBy: 1,
           });
+
+          models.ProjectMember.create({
+            userId: 40158431,
+            projectId: project1.id,
+            role: 'customer',
+            isPrimary: true,
+            createdBy: 1,
+            updatedBy: 1,
+          });
         }).then(() =>
           models.Project.create({
             type: 'generic',
@@ -147,9 +163,9 @@ describe('Project Member Invite create', () => {
       server.services.pubsub.publish.restore();
       sinon.stub(server.services.pubsub, 'init', () => {});
       sinon.stub(server.services.pubsub, 'publish', () => {});
-      // by default mock lookupUserEmails return nothing so all the cases are not broken
+      // by default mock lookupMultipleUserEmails return nothing so all the cases are not broken
       sandbox.stub(util, 'getUserRoles', () => Promise.resolve([]));
-      sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([]));
+      sandbox.stub(util, 'lookupMultipleUserEmails', () => Promise.resolve([]));
       sandbox.stub(util, 'getMemberDetailsByUserIds', () => Promise.resolve([{
         userId: 40051333,
         firstName: 'Admin',
@@ -168,7 +184,7 @@ describe('Project Member Invite create', () => {
           Authorization: `Bearer ${testUtil.jwts.admin}`,
         })
         .send({
-          userIds: [40051332],
+          userIds: [40051331],
           emails: ['hello@world.com'],
           role: 'customer',
         })
@@ -178,7 +194,7 @@ describe('Project Member Invite create', () => {
           if (err) {
             done(err);
           } else {
-            const resJson = res.body.success[0];
+            const resJson = res.body.success[1];
             should.exist(resJson);
             resJson.role.should.equal('customer');
             resJson.projectId.should.equal(project1.id);
@@ -358,8 +374,8 @@ describe('Project Member Invite create', () => {
         }),
       });
       sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
-      util.lookupUserEmails.restore();
-      sandbox.stub(util, 'lookupUserEmails', () => Promise.resolve([{
+      util.lookupMultipleUserEmails.restore();
+      sandbox.stub(util, 'lookupMultipleUserEmails', () => Promise.resolve([{
         id: '12345',
         email: 'hello@world.com',
       }]));
@@ -436,7 +452,88 @@ describe('Project Member Invite create', () => {
         });
     });
 
-    it('should return 201 and empty response when trying add already invited member', (done) => {
+    it('should return 403 and failed list when trying add already team member by userId', (done) => {
+      const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+        get: () => Promise.resolve({
+          status: 200,
+          data: {
+            success: [{
+              roleName: USER_ROLE.COPILOT,
+            }],
+          },
+        }),
+      });
+      sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
+      request(server)
+        .post(`/v5/projects/${project1.id}/members/invite`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          userIds: [40158431],
+          role: 'customer',
+        })
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body.failed;
+            should.exist(resJson);
+            resJson[0].userId.should.equal(40158431);
+            resJson[0].message.should.equal('User with such handle is already a member of the team.');
+            resJson.length.should.equal(1);
+            server.services.pubsub.publish.neverCalledWith('project.member.invite.created').should.be.true;
+            done();
+          }
+        });
+    });
+
+    it('should return 403 and failed list when trying add already team member by email', (done) => {
+      const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+        get: () => Promise.resolve({
+          status: 200,
+          data: {
+            success: [{
+              roleName: USER_ROLE.COPILOT,
+            }],
+          },
+        }),
+      });
+      sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
+      util.lookupMultipleUserEmails.restore();
+      sandbox.stub(util, 'lookupMultipleUserEmails', () => Promise.resolve([{
+        id: '40158431',
+        email: 'romit.choudhary@rivigo.com',
+      }]));
+      request(server)
+        .post(`/v5/projects/${project1.id}/members/invite`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send({
+          emails: ['romit.choudhary@rivigo.com'],
+          role: 'customer',
+        })
+        .expect('Content-Type', /json/)
+        .expect(403)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body.failed;
+            should.exist(resJson);
+            resJson[0].email.should.equal('romit.choudhary@rivigo.com');
+            resJson[0].message.should.equal('User with such email is already a member of the team.');
+            resJson.length.should.equal(1);
+            server.services.pubsub.publish.neverCalledWith('project.member.invite.created').should.be.true;
+            done();
+          }
+        });
+    });
+
+    it('should return 403 and failed list when trying add already invited member by userId', (done) => {
       const mockHttpClient = _.merge(testUtil.mockHttpClient, {
         get: () => Promise.resolve({
           status: 200,
@@ -466,14 +563,16 @@ describe('Project Member Invite create', () => {
           role: 'customer',
         })
         .expect('Content-Type', /json/)
-        .expect(201)
+        .expect(403)
         .end((err, res) => {
           if (err) {
             done(err);
           } else {
-            const resJson = res.body.success;
+            const resJson = res.body.failed;
             should.exist(resJson);
-            resJson.length.should.equal(0);
+            resJson.length.should.equal(1);
+            resJson[0].userId.should.equal(40051335);
+            resJson[0].message.should.equal('User with such handle is already invited to this project.');
             server.services.pubsub.publish.neverCalledWith('project.member.invite.created').should.be.true;
             done();
           }
@@ -657,7 +756,7 @@ describe('Project Member Invite create', () => {
         });
     });
 
-    it('should return 201 and empty response when trying add already invited member by lowercase email', (done) => {
+    it('should return 403 and failed list when trying add already invited member by lowercase email', (done) => {
       request(server)
         .post(`/v5/projects/${project1.id}/members/invite`)
         .set({
@@ -668,20 +767,22 @@ describe('Project Member Invite create', () => {
           role: 'customer',
         })
         .expect('Content-Type', /json/)
-        .expect(201)
+        .expect(403)
         .end((err, res) => {
           if (err) {
             done(err);
           } else {
-            const resJson = res.body.success;
+            const resJson = res.body.failed;
             should.exist(resJson);
-            resJson.length.should.equal(0);
+            resJson[0].email.should.equal('duplicate_lowercase@test.com');
+            resJson[0].message.should.equal('User with such email is already invited to this project.');
+            resJson.length.should.equal(1);
             done();
           }
         });
     });
 
-    it('should return 201 and empty response when trying add already invited member by uppercase email', (done) => {
+    it('should return 403 and failed list when trying add already invited member by uppercase email', (done) => {
       request(server)
         .post(`/v5/projects/${project1.id}/members/invite`)
         .set({
@@ -692,20 +793,22 @@ describe('Project Member Invite create', () => {
           role: 'customer',
         })
         .expect('Content-Type', /json/)
-        .expect(201)
+        .expect(403)
         .end((err, res) => {
           if (err) {
             done(err);
           } else {
-            const resJson = res.body.success;
+            const resJson = res.body.failed;
             should.exist(resJson);
-            resJson.length.should.equal(0);
+            resJson[0].email.should.equal('DUPLICATE_UPPERCASE@test.com');
+            resJson[0].message.should.equal('User with such email is already invited to this project.');
+            resJson.length.should.equal(1);
             done();
           }
         });
     });
 
-    xit('should return 201 and empty response when trying add already invited member by gmail email with dot',
+    xit('should return 403 and failed list when trying add already invited member by gmail email with dot',
       (done) => {
         request(server)
           .post(`/v5/projects/${project1.id}/members/invite`)
@@ -717,20 +820,21 @@ describe('Project Member Invite create', () => {
             role: 'customer',
           })
           .expect('Content-Type', /json/)
-          .expect(201)
+          .expect(403)
           .end((err, res) => {
             if (err) {
               done(err);
             } else {
-              const resJson = res.body.success;
+              const resJson = res.body.failed;
               should.exist(resJson);
-              resJson.length.should.equal(0);
+              resJson[0].email.should.equal('WITHdot@gmail.com');
+              resJson.length.should.equal(1);
               done();
             }
           });
       });
 
-    xit('should return 201 and empty response when trying add already invited member by gmail email without dot',
+    xit('should return 403 and failed list when trying add already invited member by gmail email without dot',
       (done) => {
         request(server)
           .post(`/v5/projects/${project1.id}/members/invite`)
@@ -742,14 +846,15 @@ describe('Project Member Invite create', () => {
             role: 'customer',
           })
           .expect('Content-Type', /json/)
-          .expect(201)
+          .expect(403)
           .end((err, res) => {
             if (err) {
               done(err);
             } else {
-              const resJson = res.body.success;
+              const resJson = res.body.failed;
               should.exist(resJson);
-              resJson.length.should.equal(0);
+              resJson.length.should.equal(1);
+              resJson[0].email.should.equal('WITHOUT.dot@gmail.com');
               done();
             }
           });
@@ -767,21 +872,13 @@ describe('Project Member Invite create', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED message when userId invite added', (done) => {
+      it('should send correct BUS API messages when invite added by userId', (done) => {
         const mockHttpClient = _.merge(testUtil.mockHttpClient, {
           get: () => Promise.resolve({
             status: 200,
-            data: {
-              id: 'requesterId',
-              version: 'v3',
-              result: {
-                success: true,
-                status: 200,
-                content: [{
-                  roleName: USER_ROLE.MANAGER,
-                }],
-              },
-            },
+            data: [{
+              roleName: USER_ROLE.MANAGER,
+            }],
           }),
         });
         sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
@@ -800,37 +897,36 @@ describe('Project Member Invite create', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ resource: RESOURCES.PROJECT_MEMBER_INVITE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ projectId: project1.id })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ userId: 3 })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                  sinon.match({ email: null })).should.be.true;
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER_INVITE,
+                projectId: project1.id,
+                userId: 3,
+                email: null,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({
+                projectId: project1.id,
+                userId: 3,
+                email: null,
+                isSSO: false,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('sends BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED message when email invite added', (done) => {
+      it('should send correct BUS API messages when invite added by email', (done) => {
         const mockHttpClient = _.merge(testUtil.mockHttpClient, {
           get: () => Promise.resolve({
             status: 200,
-            data: {
-              id: 'requesterId',
-              version: 'v3',
-              result: {
-                success: true,
-                status: 200,
-                content: [{
-                  roleName: USER_ROLE.MANAGER,
-                }],
-              },
-            },
+            data: [{
+              roleName: USER_ROLE.MANAGER,
+            }],
           }),
         });
         sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
@@ -849,16 +945,26 @@ describe('Project Member Invite create', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ resource: RESOURCES.PROJECT_MEMBER_INVITE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ projectId: project1.id })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                sinon.match({ userId: null })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED,
-                  sinon.match({ email: 'hello@world.com' })).should.be.true;
+              createEventSpy.callCount.should.be.eql(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER_INVITE,
+                projectId: project1.id,
+                userId: null,
+                email: 'hello@world.com',
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({
+                projectId: project1.id,
+                userId: null,
+                email: 'hello@world.com',
+                isSSO: false,
+              })).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_EMAIL_INVITE_CREATED, sinon.match({
+                recipients: ['hello@world.com'],
+              })).should.be.true;
+
               done();
             });
           }
diff --git a/src/routes/projectMemberInvites/update.spec.js b/src/routes/projectMemberInvites/update.spec.js
index 4080ab4..49e53e3 100644
--- a/src/routes/projectMemberInvites/update.spec.js
+++ b/src/routes/projectMemberInvites/update.spec.js
@@ -8,7 +8,14 @@ import server from '../../app';
 import util from '../../util';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES, USER_ROLE, PROJECT_MEMBER_ROLE, INVITE_STATUS } from '../../constants';
+import {
+  BUS_API_EVENT,
+  RESOURCES,
+  USER_ROLE,
+  PROJECT_MEMBER_ROLE,
+  INVITE_STATUS,
+  CONNECT_NOTIFICATION_EVENT,
+} from '../../constants';
 
 const should = chai.should();
 
@@ -301,21 +308,11 @@ describe('Project member invite update', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('Accept invite sends BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED ' +
-          'and BUS_API_EVENT.PROJECT_MEMBER_ADDED messages', (done) => {
+      it('should send correct BUS API messages when invite is accepted', (done) => {
         const mockHttpClient = _.merge(testUtil.mockHttpClient, {
           get: () => Promise.resolve({
             status: 200,
-            data: {
-              id: 'requesterId',
-              version: 'v3',
-              result: {
-                success: true,
-                status: 200,
-                content: [{
-                }],
-              },
-            },
+            data: {},
           }),
         });
         sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
@@ -335,18 +332,51 @@ describe('Project member invite update', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED,
-                sinon.match({ resource: RESOURCES.PROJECT_MEMBER_INVITE })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED,
-                sinon.match({ projectId: project1.id })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED,
-                sinon.match({ userId: invite1.userId })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED,
-                  sinon.match({ status: INVITE_STATUS.ACCEPTED })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED,
-                  sinon.match({ email: null })).should.be.true;
+              createEventSpy.callCount.should.be.eql(5);
+
+              /*
+                Events for accepted invite
+              */
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER_INVITE,
+                projectId: project1.id,
+                userId: invite1.userId,
+                status: INVITE_STATUS.ACCEPTED,
+                email: null,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_UPDATED, sinon.match({
+                projectId: project1.id,
+                userId: invite1.userId,
+                status: INVITE_STATUS.ACCEPTED,
+                email: null,
+                isSSO: false,
+              })).should.be.true;
+
+              /*
+                Events for created member (after invite acceptance)
+              */
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_ADDED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER,
+                projectId: project1.id,
+                userId: invite1.userId,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                userId: invite1.userId,
+                initiatorUserId: 40051331,
+              })).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                userId: invite1.userId,
+                initiatorUserId: 40051331,
+              })).should.be.true;
+
               done();
             });
           }
diff --git a/src/routes/projectMembers/create.js b/src/routes/projectMembers/create.js
index 31ba995..cb72ebc 100644
--- a/src/routes/projectMembers/create.js
+++ b/src/routes/projectMembers/create.js
@@ -3,7 +3,7 @@ import Joi from 'joi';
 import validate from 'express-validation';
 import { middleware as tcMiddleware } from 'tc-core-library-js';
 import util from '../../util';
-import { INVITE_STATUS, MANAGER_ROLES, PROJECT_MEMBER_ROLE, USER_ROLE, EVENT, RESOURCES } from '../../constants';
+import { INVITE_STATUS, MANAGER_ROLES, PROJECT_MEMBER_ROLE, USER_ROLE } from '../../constants';
 import models from '../../models';
 
 /**
@@ -16,7 +16,15 @@ const permissions = tcMiddleware.permissions;
 const createProjectMemberValidations = {
   body: Joi.object().keys({
     role: Joi.any()
-          .valid(PROJECT_MEMBER_ROLE.MANAGER, PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER, PROJECT_MEMBER_ROLE.COPILOT),
+      .valid(
+        PROJECT_MEMBER_ROLE.MANAGER,
+        PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER,
+        PROJECT_MEMBER_ROLE.COPILOT,
+        PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+        PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+        PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+        PROJECT_MEMBER_ROLE.ACCOUNT_EXECUTIVE,
+    ),
   }),
 };
 
@@ -36,9 +44,49 @@ module.exports = [
         return next(err);
       }
 
+      if (PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT === targetRole &&
+          !util.hasRoles(req, [USER_ROLE.SOLUTION_ARCHITECT])) {
+        const err = new Error(`Only solution architect is able to join as ${targetRole}`);
+        err.status = 401;
+        return next(err);
+      }
+
+      if (PROJECT_MEMBER_ROLE.PROJECT_MANAGER === targetRole &&
+          !util.hasRoles(req, [USER_ROLE.PROJECT_MANAGER])) {
+        const err = new Error(`Only project manager is able to join as ${targetRole}`);
+        err.status = 401;
+        return next(err);
+      }
+
+      if (PROJECT_MEMBER_ROLE.PROGRAM_MANAGER === targetRole &&
+          !util.hasRoles(req, [USER_ROLE.PROGRAM_MANAGER])) {
+        const err = new Error(`Only program manager is able to join as ${targetRole}`);
+        err.status = 401;
+        return next(err);
+      }
+
+      if (PROJECT_MEMBER_ROLE.ACCOUNT_EXECUTIVE === targetRole &&
+          !util.hasRoles(req, [USER_ROLE.ACCOUNT_EXECUTIVE])) {
+        const err = new Error(`Only account executive is able to join as ${targetRole}`);
+        err.status = 401;
+        return next(err);
+      }
+
       if (PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER === targetRole &&
-        !util.hasRoles(req, [USER_ROLE.MANAGER, USER_ROLE.TOPCODER_ACCOUNT_MANAGER])) {
-        const err = new Error(`Only manager  or account manager is able to join as ${targetRole}`);
+        !util.hasRoles(req, [
+          USER_ROLE.MANAGER,
+          USER_ROLE.TOPCODER_ACCOUNT_MANAGER,
+          USER_ROLE.BUSINESS_DEVELOPMENT_REPRESENTATIVE,
+          USER_ROLE.PRESALES,
+          USER_ROLE.ACCOUNT_EXECUTIVE,
+          USER_ROLE.PROGRAM_MANAGER,
+          USER_ROLE.SOLUTION_ARCHITECT,
+          USER_ROLE.PROJECT_MANAGER,
+        ])) {
+        const err = new Error(
+            // eslint-disable-next-line max-len
+            `Only manager, account manager, business development representative, account executive, program manager, project manager, solution architect, or presales are able to join as ${targetRole}`,
+        );
         err.status = 401;
         return next(err);
       }
@@ -50,10 +98,22 @@ module.exports = [
       }
     } else if (util.hasRoles(req, [USER_ROLE.MANAGER, USER_ROLE.CONNECT_ADMIN])) {
       targetRole = PROJECT_MEMBER_ROLE.MANAGER;
-    } else if (util.hasRoles(req, [USER_ROLE.TOPCODER_ACCOUNT_MANAGER])) {
+    } else if (util.hasRoles(req, [
+      USER_ROLE.TOPCODER_ACCOUNT_MANAGER,
+      USER_ROLE.BUSINESS_DEVELOPMENT_REPRESENTATIVE,
+      USER_ROLE.PRESALES,
+    ])) {
       targetRole = PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER;
     } else if (util.hasRoles(req, [USER_ROLE.COPILOT, USER_ROLE.CONNECT_ADMIN])) {
       targetRole = PROJECT_MEMBER_ROLE.COPILOT;
+    } else if (util.hasRoles(req, [USER_ROLE.ACCOUNT_EXECUTIVE])) {
+      targetRole = PROJECT_MEMBER_ROLE.ACCOUNT_EXECUTIVE;
+    } else if (util.hasRoles(req, [USER_ROLE.PROGRAM_MANAGER])) {
+      targetRole = PROJECT_MEMBER_ROLE.PROGRAM_MANAGER;
+    } else if (util.hasRoles(req, [USER_ROLE.SOLUTION_ARCHITECT])) {
+      targetRole = PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT;
+    } else if (util.hasRoles(req, [USER_ROLE.PROJECT_MANAGER])) {
+      targetRole = PROJECT_MEMBER_ROLE.PROJECT_MANAGER;
     } else {
       const err = new Error('Only copilot or manager is able to call this endpoint');
       err.status = 401;
@@ -85,37 +145,19 @@ module.exports = [
         return next(err);
       }
 
-      return util.addUserToProject(req, member)
-        .then((newMember) => {
-          let invite;
-          return models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, null, newMember.userId)
-            .then((_invite) => {
-              invite = _invite;
+      return util.addUserToProject(req, member) // Kafka event is emitted inside `addUserToProject`
+        .then(newMember =>
+          models.ProjectMemberInvite.getPendingInviteByEmailOrUserId(projectId, null, newMember.userId)
+            .then((invite) => {
               if (!invite) {
-                // emit the event
-                util.sendResourceToKafkaBus(
-                  req,
-                  EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED,
-                  RESOURCES.PROJECT_MEMBER,
-                  newMember);
-
-                return res.status(201)
-                  .json(newMember);
+                return res.status(201).json(newMember);
               }
               return invite.update({
                 status: INVITE_STATUS.ACCEPTED,
               })
-                .then(() => {
-                  // emit the event
-                  util.sendResourceToKafkaBus(
-                    req,
-                    EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED,
-                    RESOURCES.PROJECT_MEMBER,
-                    newMember);
-                  return res.status(201).json(newMember);
-                });
-            });
-        });
+                .then(() => res.status(201).json(newMember));
+            }),
+        );
     })
       .catch(err => next(err));
   },
diff --git a/src/routes/projectMembers/create.spec.js b/src/routes/projectMembers/create.spec.js
index 21e05f6..7e19399 100644
--- a/src/routes/projectMembers/create.spec.js
+++ b/src/routes/projectMembers/create.spec.js
@@ -8,7 +8,8 @@ import models from '../../models';
 import util from '../../util';
 import server from '../../app';
 import testUtil from '../../tests/util';
-import { USER_ROLE } from '../../constants';
+import busApi from '../../services/busApi';
+import { USER_ROLE, BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT, INVITE_STATUS } from '../../constants';
 
 const should = chai.should();
 
@@ -199,5 +200,164 @@ describe('Project Members create', () => {
           }
         });
     });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      it('should send correct BUS API messages when a manager added', (done) => {
+        const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+          get: () => Promise.resolve({
+            status: 200,
+            data: {
+              id: 'requesterId',
+              version: 'v3',
+              result: {
+                success: true,
+                status: 200,
+                content: [{
+                  roleName: USER_ROLE.MANAGER,
+                }],
+              },
+            },
+          }),
+          post: () => Promise.resolve({
+            status: 200,
+            data: {
+              id: 'requesterId',
+              version: 'v3',
+              result: {
+                success: true,
+                status: 200,
+                content: {},
+              },
+            },
+          }),
+        });
+        sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
+        request(server)
+        .post(`/v5/projects/${project1.id}/members/`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(201)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_ADDED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER,
+                projectId: project1.id,
+                userId: 40051334,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED_MANAGER).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051334,
+                initiatorUserId: 40051334,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when copilot added', (done) => {
+        request(server)
+        .post(`/v5/projects/${project1.id}/members/invite`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          userIds: [40051332],
+          role: 'copilot',
+        })
+        .expect(201)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            request(server)
+            .put(`/v5/projects/${project1.id}/members/invite`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+            })
+            .send({
+              userId: 40051332,
+              status: 'accepted',
+            })
+            .expect('Content-Type', /json/)
+            .expect(200)
+            .end((err2) => {
+              if (err2) {
+                done(err2);
+              } else {
+                testUtil.wait(() => {
+                  createEventSpy.callCount.should.equal(7);
+
+                  /*
+                    Copilot invitation requested
+                  */
+                  createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_CREATED, sinon.match({
+                    resource: RESOURCES.PROJECT_MEMBER_INVITE,
+                    projectId: project1.id,
+                    userId: 40051332,
+                    email: null,
+                  })).should.be.true;
+
+                  // Check Notification Service events
+                  createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_REQUESTED).should.be.true;
+
+                  /*
+                    Copilot invitation accepted
+                  */
+                  createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_INVITE_UPDATED, sinon.match({
+                    resource: RESOURCES.PROJECT_MEMBER_INVITE,
+                    projectId: project1.id,
+                    userId: 40051332,
+                    status: INVITE_STATUS.ACCEPTED,
+                    email: null,
+                  })).should.be.true;
+
+                  createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_ADDED, sinon.match({
+                    resource: RESOURCES.PROJECT_MEMBER,
+                    projectId: project1.id,
+                    userId: 40051332,
+                  })).should.be.true;
+
+                  // Check Notification Service events
+                  createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_MEMBER_INVITE_UPDATED).should.be.true;
+                  createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MEMBER_JOINED_COPILOT).should.be.true;
+                  createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                    projectId: project1.id,
+                    projectName: project1.name,
+                    projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                    userId: 40051336,
+                    initiatorUserId: 40051336,
+                  })).should.be.true;
+                  done();
+                });
+              }
+            });
+          }
+        });
+      });
+    });
   });
 });
diff --git a/src/routes/projectMembers/delete.js b/src/routes/projectMembers/delete.js
index 1a14548..d13a8f7 100644
--- a/src/routes/projectMembers/delete.js
+++ b/src/routes/projectMembers/delete.js
@@ -83,7 +83,7 @@ module.exports = [
           req,
           EVENT.ROUTING_KEY.PROJECT_MEMBER_REMOVED,
           RESOURCES.PROJECT_MEMBER,
-          { id: pmember.id });
+          pmember);
         res.status(204).json({});
       }).catch(err => next(err));
   },
diff --git a/src/routes/projectMembers/delete.spec.js b/src/routes/projectMembers/delete.spec.js
index 5eddc35..dd1556a 100644
--- a/src/routes/projectMembers/delete.spec.js
+++ b/src/routes/projectMembers/delete.spec.js
@@ -9,7 +9,7 @@ import util from '../../util';
 import server from '../../app';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -328,7 +328,7 @@ describe('Project members delete', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends BUS_API_EVENT.PROJECT_MEMBER_REMOVED message when manager removed', (done) => {
+      it('should send correct BUS API messages when manager left', (done) => {
         const mockHttpClient = _.merge(testUtil.mockHttpClient, {
           post: () => Promise.resolve({
             status: 200,
@@ -355,19 +355,30 @@ describe('Project members delete', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED,
-                  sinon.match({ resource: RESOURCES.PROJECT_MEMBER })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED,
-                  sinon.match({ id: member2.id })).should.be.true;
+                createEventSpy.callCount.should.equal(3);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED, sinon.match({
+                  resource: RESOURCES.PROJECT_MEMBER,
+                  id: member2.id,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MEMBER_LEFT).should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                  projectId: project1.id,
+                  projectName: project1.name,
+                  projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                  userId: 40051334,
+                  initiatorUserId: 40051334,
+                })).should.be.true;
+
                 done();
               });
             }
           });
       });
 
-      it('sends BUS_API_EVENT.PROJECT_MEMBER_REMOVED message when copilot removed', (done) => {
+      it('should send correct BUS API messages when copilot removed', (done) => {
         request(server)
           .delete(`/v5/projects/${project1.id}/members/${member1.id}`)
           .set({
@@ -379,12 +390,23 @@ describe('Project members delete', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED,
-                  sinon.match({ resource: RESOURCES.PROJECT_MEMBER })).should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED,
-                  sinon.match({ id: member1.id })).should.be.true;
+                createEventSpy.callCount.should.equal(3);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_REMOVED, sinon.match({
+                  resource: RESOURCES.PROJECT_MEMBER,
+                  id: member1.id,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.MEMBER_REMOVED).should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                  projectId: project1.id,
+                  projectName: project1.name,
+                  projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                  userId: 40051334,
+                  initiatorUserId: 40051334,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/projectMembers/get.spec.js b/src/routes/projectMembers/get.spec.js
index fd43ed2..bde598a 100644
--- a/src/routes/projectMembers/get.spec.js
+++ b/src/routes/projectMembers/get.spec.js
@@ -33,6 +33,7 @@ describe('GET project member', () => {
 
   beforeEach((done) => {
     testUtil.clearDb()
+      .then(() => testUtil.clearES())
       .then(() => {
         // Create projects
         models.Project.create({
diff --git a/src/routes/projectMembers/update.js b/src/routes/projectMembers/update.js
index 68aa0cc..48d976b 100644
--- a/src/routes/projectMembers/update.js
+++ b/src/routes/projectMembers/update.js
@@ -15,8 +15,17 @@ const permissions = tcMiddleware.permissions;
 const updateProjectMemberValdiations = {
   body: Joi.object().keys({
     isPrimary: Joi.boolean(),
-    role: Joi.any().valid(PROJECT_MEMBER_ROLE.CUSTOMER, PROJECT_MEMBER_ROLE.MANAGER,
-        PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER, PROJECT_MEMBER_ROLE.COPILOT, PROJECT_MEMBER_ROLE.OBSERVER).required(),
+    role: Joi.any().valid(
+      PROJECT_MEMBER_ROLE.CUSTOMER,
+      PROJECT_MEMBER_ROLE.MANAGER,
+      PROJECT_MEMBER_ROLE.ACCOUNT_MANAGER,
+      PROJECT_MEMBER_ROLE.COPILOT,
+      PROJECT_MEMBER_ROLE.OBSERVER,
+      PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+      PROJECT_MEMBER_ROLE.ACCOUNT_EXECUTIVE,
+      PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
+      PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+    ).required(),
   }),
 };
 
@@ -104,7 +113,8 @@ module.exports = [
               req,
               EVENT.ROUTING_KEY.PROJECT_MEMBER_UPDATED,
               RESOURCES.PROJECT_MEMBER,
-              projectMember);
+              projectMember,
+              previousValue);
           req.log.debug('updated project member', projectMember);
           res.json(projectMember);
         })
diff --git a/src/routes/projectMembers/update.spec.js b/src/routes/projectMembers/update.spec.js
index 38594b6..28c3d66 100644
--- a/src/routes/projectMembers/update.spec.js
+++ b/src/routes/projectMembers/update.spec.js
@@ -8,7 +8,7 @@ import server from '../../app';
 import util from '../../util';
 import testUtil from '../../tests/util';
 import busApi from '../../services/busApi';
-import { BUS_API_EVENT, RESOURCES } from '../../constants';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 
 const should = chai.should();
 
@@ -477,7 +477,7 @@ describe('Project members update', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_MEMBER_UPDATED message when user role updated', (done) => {
+      it('should send correct BUS API messages when user role updated', (done) => {
         const mockHttpClient = _.merge(testUtil.mockHttpClient, {
           get: () => Promise.resolve({
             status: 200,
@@ -508,16 +508,24 @@ describe('Project members update', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED,
-                sinon.match({ resource: RESOURCES.PROJECT_MEMBER })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED,
-                sinon.match({ id: member2.id })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED,
-                sinon.match({ role: 'customer' })).should.be.true;
-              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED,
-                  sinon.match({ userId: 40051332 })).should.be.true;
+              createEventSpy.callCount.should.equal(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_MEMBER_UPDATED, sinon.match({
+                resource: RESOURCES.PROJECT_MEMBER,
+                id: member2.id,
+                role: 'customer',
+                userId: 40051332,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_TEAM_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051332,
+                initiatorUserId: 40051332,
+              })).should.be.true;
+
               done();
             });
           }
diff --git a/src/routes/projectReports/LookAuth.js b/src/routes/projectReports/LookAuth.js
new file mode 100644
index 0000000..745d318
--- /dev/null
+++ b/src/routes/projectReports/LookAuth.js
@@ -0,0 +1,73 @@
+/* eslint-disable func-names */
+/* eslint-disable require-jsdoc */
+/* eslint-disable valid-jsdoc */
+
+// Look Auth
+
+
+import config from 'config';
+
+const axios = require('axios');
+
+const NEXT_5_MINS = 5 * 60 * 1000;
+
+
+function LookAuth(logger) {
+  // load credentials from config
+  this.BASE_URL = config.lookerConfig.BASE_URL;
+  this.CLIENT_ID = config.lookerConfig.CLIENT_ID;
+  this.CLIENT_SECRET = config.lookerConfig.CLIENT_SECRET;
+  const token = config.lookerConfig.TOKEN;
+
+  this.logger = logger;
+
+  // Token is stringified and saved as string. It has 4 properties, access_token, expires_in and type, timestamp
+  if (token) {
+    this.lastToken = JSON.stringify(token);
+  }
+}
+
+LookAuth.prototype.getToken = async function () {
+  const res = await new Promise((resolve) => {
+    if (!this.isExpired()) {
+      resolve(this.lastToken.access_token);
+    } else {
+      resolve('');
+    }
+  });
+  if (res === '') {
+    return this.login();
+  }
+  return res;
+};
+
+/** *********************Login to Looker ************** */
+LookAuth.prototype.login = async function () {
+  const loginUrl = `${this.BASE_URL}/login?client_id=${this.CLIENT_ID}&client_secret=${this.CLIENT_SECRET}`;
+  const res = await axios.post(loginUrl, {}, { headers: { 'Content-Type': 'application/json' } });
+  this.lastToken = res.data;
+  this.lastToken.timestamp = new Date().getTime();
+  return this.lastToken.access_token;
+};
+
+
+/** ***************Check if the Token has expired ********** */
+LookAuth.prototype.isExpired = function () {
+  // If no token is present, assume the token has expired
+  if (!this.lastToken) {
+    return true;
+  }
+
+  const tokenTimestamp = this.lastToken.timestamp;
+  const expiresIn = this.lastToken.expires_in;
+  const currentTimestamp = new Date().getTime();
+
+  // If the token will good for next 5 minutes
+  if ((tokenTimestamp + expiresIn + NEXT_5_MINS) > currentTimestamp) {
+    return false;
+  }
+  // Token is good, and can be used to make the next call.
+  return true;
+};
+
+module.exports = LookAuth;
diff --git a/src/routes/projectReports/LookRun.js b/src/routes/projectReports/LookRun.js
new file mode 100644
index 0000000..f1606d8
--- /dev/null
+++ b/src/routes/projectReports/LookRun.js
@@ -0,0 +1,106 @@
+/* eslint-disable valid-jsdoc */
+/* eslint-disable require-jsdoc */
+/* eslint-disable func-names */
+
+import config from 'config';
+import LookAuth from './LookAuth';
+
+const axios = require('axios');
+
+function LookApi(logger) {
+  this.BASE_URL = config.lookerConfig.BASE_URL;
+  this.formatting = 'json';
+  this.limit = 5000;
+  this.logger = logger;
+  this.lookAuth = new LookAuth(logger);
+}
+
+LookApi.prototype.runLook = function (lookId) {
+  const endpoint = `${this.BASE_URL}/looks/${lookId}/run/${this.formatting}?limit=${this.limit}`;
+  return this.callApi(endpoint);
+};
+
+LookApi.prototype.findUserByEmail = function (email) {
+  const filter = { 'user.email': email };
+  return this.runQueryWithFilter(1234, filter);
+};
+
+LookApi.prototype.findByHandle = function (handle) {
+  const filter = { 'user.handle': handle };
+  return this.runQueryWithFilter(12345, filter);
+};
+
+LookApi.prototype.findProjectRegSubmissions = function (projectId) {
+  const queryId = config.lookerConfig.QUERIES.REG_STATS;
+  const fields = ['connect_project.id', 'challenge.track', 'challenge.num_registrations', 'challenge.num_submissions'];
+  const view = 'challenge';
+  const filters = { 'connect_project.id': projectId };
+  return this.runQueryWithFilter(queryId, view, fields, filters);
+};
+
+LookApi.prototype.findProjectBudget = function (connectProjectId, permissions) {
+  const queryId = config.lookerConfig.QUERIES.BUDGET;
+  const { isManager, isAdmin, isCopilot, isCustomer } = permissions;
+
+  const fields = [
+    'project_stream.tc_connect_project_id',
+  ];
+
+  // Manager roles have access to more fields.
+  if (isManager || isAdmin) {
+    fields.push('project_stream.total_actual_challenge_fee');
+  }
+  if (isManager || isAdmin || isCopilot) {
+    fields.push('project_stream.total_actual_member_payment');
+  }
+  if (isManager || isAdmin || isCustomer) {
+    fields.push('project_stream.total_invoiced_amount', 'project_stream.remaining_invoiced_budget');
+  }
+  const view = 'project_stream';
+  const filters = { 'project_stream.tc_connect_project_id': connectProjectId };
+  return this.runQueryWithFilter(queryId, view, fields, filters);
+};
+
+LookApi.prototype.runQueryWithFilter = function (queryId, view, fields, filters) {
+  const endpoint = `${this.BASE_URL}/queries/run/${this.formatting}`;
+
+  const body = {
+    id: queryId,
+    model: 'topcoder_model_main',
+    view,
+    filters,
+    fields,
+    // sorts: ['user.email desc 0'],
+    limit: 10,
+    query_timezon: 'America/Los_Angeles',
+
+  };
+  return this.callApi(endpoint, body);
+};
+
+LookApi.prototype.runQuery = function (queryId) {
+  const endpoint = `${this.BASE_URL}/queries/${queryId}/run/${this.formatting}?limit=${this.limit}`;
+  return this.callApi(endpoint);
+};
+
+LookApi.prototype.callApi = function (endpoint, body) {
+  return this.lookAuth.getToken().then((token) => {
+    let newReq = null;
+    if (body) {
+      newReq = axios.post(endpoint, body, {
+        headers: {
+          'Content-Type': 'application/json',
+          Authorization: `token ${token}`,
+        },
+      });
+    } else {
+      newReq = axios.get(endpoint);
+    }
+    return newReq;
+  }).then((res) => {
+    this.logger.info(res.data);
+    return res.data;
+  });
+};
+
+module.exports = LookApi;
diff --git a/src/routes/projectReports/getReport.js b/src/routes/projectReports/getReport.js
new file mode 100644
index 0000000..aa4253c
--- /dev/null
+++ b/src/routes/projectReports/getReport.js
@@ -0,0 +1,56 @@
+/* eslint-disable no-unused-vars */
+import config from 'config';
+import _ from 'lodash';
+
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import LookApi from './LookRun';
+import mock from './mock';
+import util from '../../util';
+import { PROJECT_MEMBER_MANAGER_ROLES, USER_ROLE, PROJECT_MEMBER_ROLE } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+
+module.exports = [
+  permissions('project.view'),
+  async (req, res, next) => {
+    const projectId = Number(req.params.projectId);
+    const reportName = req.query.reportName;
+
+    if (config.lookerConfig.USE_MOCK === 'true') {
+      req.log.info('using mock');
+      // using mock
+      return mock(projectId, reportName, req, res);
+      // res.status(200).json(util.wrapResponse(req.id, project));
+    }
+    const lookApi = new LookApi(req.log);
+
+    try {
+      // check if auth user has acecss to this project
+      const members = req.context.currentProjectMembers;
+      const member = _.find(members, m => m.userId === req.authUser.userId);
+      const isManager = member && PROJECT_MEMBER_MANAGER_ROLES.indexOf(member.role) > -1;
+      const isAdmin = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN]);
+      const isCopilot = member && member.role === PROJECT_MEMBER_ROLE.COPILOT;
+      const isCustomer = member && member.role === PROJECT_MEMBER_ROLE.CUSTOMER;
+      // pick the report based on its name
+      let result = {};
+      switch (reportName) {
+        case 'summary':
+          result = await lookApi.findProjectRegSubmissions(projectId);
+          break;
+        case 'projectBudget':
+          result = await lookApi.findProjectBudget(projectId, { isManager, isAdmin, isCopilot, isCustomer });
+          break;
+        default:
+          return res.status(404).send('Report not found');
+      }
+
+      req.log.debug(result);
+      return res.status(200).json(result);
+    } catch (err) {
+      req.log.error(err);
+      return res.status(500).send(err.toString());
+    }
+  },
+];
diff --git a/src/routes/projectReports/mock.js b/src/routes/projectReports/mock.js
new file mode 100644
index 0000000..33e6787
--- /dev/null
+++ b/src/routes/projectReports/mock.js
@@ -0,0 +1,25 @@
+import _ from 'lodash';
+import util from '../../util';
+
+const summaryJson = require('./mockFiles/summary.json');
+let projectBudgetJson = require('./mockFiles/projectBudget.json');
+
+module.exports = (projectId, reportName, req, res) => {
+  if (Number(projectId) === 123456) {
+    res.status(500).json('Invalid project id');
+  }
+
+  switch (reportName) {
+    case 'summary':
+      res.status(200).json(util.wrapResponse(req.id, summaryJson));
+      break;
+    case 'projectBudget': {
+      const augmentProjectId = pb => _.assign(pb, { 'project_stream.tc_connect_project_id': projectId });
+      projectBudgetJson = _.map(projectBudgetJson, augmentProjectId);
+      res.status(200).json(util.wrapResponse(req.id, projectBudgetJson));
+      break;
+    }
+    default:
+      res.status(400).json('Invalid report name');
+  }
+};
diff --git a/src/routes/projectReports/mockFiles/projectBudget.json b/src/routes/projectReports/mockFiles/projectBudget.json
new file mode 100644
index 0000000..7d8ec03
--- /dev/null
+++ b/src/routes/projectReports/mockFiles/projectBudget.json
@@ -0,0 +1,57 @@
+[
+  {
+    "project_stream.tc_connect_project_id": "1",
+    "connect_project.directprojectid": "54321",
+    "connect_project.id": "12345",
+    "project_stream.id": "abcd",
+    "project_stream.total_approved_budget": "63680",
+    "project_stream.total_invoiced_amount": "43680",
+    "project_stream.remaining_invoiced_budget": "20000",
+    "project_stream.total_actual_challenge_fee": "29648.09",
+    "project_stream.total_actual_member_payment": "16351.91"
+  },
+  {
+    "project_stream.tc_connect_project_id": "2",
+    "connect_project.directprojectid": "54321",
+    "connect_project.id": "12345",
+    "project_stream.id": "defg",
+    "project_stream.total_approved_budget": "10000",
+    "project_stream.total_invoiced_amount": "10000",
+    "project_stream.remaining_invoiced_budget": "0",
+    "project_stream.total_actual_challenge_fee": "0",
+    "project_stream.total_actual_member_payment": "0"
+  },
+  {
+    "project_stream.tc_connect_project_id": "3",
+    "connect_project.directprojectid": "54321",
+    "connect_project.id": "12345",
+    "project_stream.id": "hijk",
+    "project_stream.total_approved_budget": "4,300",
+    "project_stream.total_invoiced_amount": "0",
+    "project_stream.remaining_invoiced_budget": "0",
+    "project_stream.total_actual_challenge_fee": "0",
+    "project_stream.total_actual_member_payment": "0"
+  },
+  {
+    "project_stream.tc_connect_project_id": "4",
+    "connect_project.directprojectid": "54321",
+    "connect_project.id": "12345",
+    "project_stream.id": "klmn",
+    "project_stream.total_approved_budget": "1250",
+    "project_stream.total_invoiced_amount": "0",
+    "project_stream.remaining_invoiced_budget": "0",
+    "project_stream.total_actual_challenge_fee": "0",
+    "project_stream.total_actual_member_payment": "0"
+  },
+  {
+    "project_stream.tc_connect_project_id": "5",
+    "connect_project.directprojectid": "54321",
+    "connect_project.id": "12345",
+    "project_stream.id": "opqu",
+    "project_stream.total_approved_budget": "0",
+    "project_stream.total_invoiced_amount": "0",
+    "project_stream.remaining_invoiced_budget": "0",
+    "project_stream.total_actual_challenge_fee": "0",
+    "project_stream.total_actual_member_payment": "0"
+  }
+]
\ No newline at end of file
diff --git a/src/routes/projectReports/mockFiles/summary.json b/src/routes/projectReports/mockFiles/summary.json
new file mode 100644
index 0000000..b97c953
--- /dev/null
+++ b/src/routes/projectReports/mockFiles/summary.json
@@ -0,0 +1,17 @@
+[ { "challenge.track": "Develop",
+
+   "challenge.num_registrations": 399,
+
+   "challenge.num_submissions": 72 },
+
+ { "challenge.track": "Design",
+
+   "challenge.num_registrations": 54,
+
+   "challenge.num_submissions": 28 },
+
+ { "challenge.track": null,
+
+   "challenge.num_registrations": 453,
+
+   "challenge.num_submissions": 100 } ]
\ No newline at end of file
diff --git a/src/routes/projectSettings/create.js b/src/routes/projectSettings/create.js
new file mode 100644
index 0000000..6623a72
--- /dev/null
+++ b/src/routes/projectSettings/create.js
@@ -0,0 +1,91 @@
+/**
+ * API to add a project setting
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+import { VALUE_TYPE } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    key: Joi.string().max(255).required(),
+    value: Joi.string().max(255).required(),
+    valueType: Joi.string().valid(_.values(VALUE_TYPE)).required(),
+    projectId: Joi.any().strip(),
+    metadata: Joi.object().optional(),
+    readPermission: Joi.object().required(),
+    writePermission: Joi.object().required(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('projectSetting.create'),
+  (req, res, next) => {
+    let setting = null;
+    const projectId = req.params.projectId;
+    const entity = _.assign(req.body, {
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+      projectId,
+    });
+
+    // Check if project exists
+    models.sequelize.transaction(() =>
+      models.Project.findOne({ where: { id: projectId } })
+        .then((project) => {
+          if (!project) {
+            const apiErr = new Error(`Project not found for id ${projectId}`);
+            apiErr.status = 404;
+            return Promise.reject(apiErr);
+          }
+
+          // Find project setting
+          return models.ProjectSetting.findOne({
+            includeAllProjectSettingsForInternalUsage: true,
+            where: {
+              projectId,
+              key: req.body.key,
+            },
+            paranoid: false,
+          });
+        })
+        .then((projectSetting) => {
+          if (projectSetting) {
+            const apiErr = new Error(`Project Setting already exists for project id ${projectId} ` +
+              `and key ${req.body.key}`);
+            apiErr.status = 400;
+            return Promise.reject(apiErr);
+          }
+
+          // Create
+          return models.ProjectSetting.create(entity);
+        })
+        .then(async (createdEntity) => {
+          setting = createdEntity;
+          // Calculate for valid estimation type
+          if (util.isProjectSettingForEstimation(createdEntity.key)) {
+            req.log.debug(`Recalculate price breakdown for project id ${projectId}`);
+            return util.calculateProjectEstimationItems(req, projectId);
+          }
+
+          return Promise.resolve();
+        }),
+      ) // transaction end
+      .then(() => {
+        req.log.debug('new project setting created (id# %d, key: %s)',
+          setting.id, setting.key);
+        // Omit deletedAt, deletedBy
+        res.status(201).json(_.omit(setting.toJSON(), 'deletedAt', 'deletedBy'));
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/projectSettings/create.spec.js b/src/routes/projectSettings/create.spec.js
new file mode 100644
index 0000000..152ee7f
--- /dev/null
+++ b/src/routes/projectSettings/create.spec.js
@@ -0,0 +1,380 @@
+/**
+ * Tests for create.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import server from '../../app';
+import testUtil from '../../tests/util';
+import models from '../../models';
+import { VALUE_TYPE } from '../../constants';
+
+const should = chai.should();
+
+const expectAfterCreate = (id, projectId, estimation, len, deletedLen, err, next) => {
+  if (err) throw err;
+
+  models.ProjectSetting.findOne({
+    includeAllProjectSettingsForInternalUsage: true,
+    where: {
+      id,
+      projectId,
+    },
+  })
+    .then((res) => {
+      if (!res) {
+        throw new Error('Should found the entity');
+      } else {
+        // find deleted ProjectEstimationItems for project
+        models.ProjectEstimationItem.findAllByProject(models, projectId, {
+          where: {
+            deletedAt: { $ne: null },
+          },
+          includeAllProjectEstimatinoItemsForInternalUsage: true,
+          paranoid: false,
+        }).then((items) => {
+          // deleted project estimation items
+          items.should.have.lengthOf(deletedLen, 'Number of deleted ProjectEstimationItems doesn\'t match');
+
+          _.each(items, (item) => {
+            should.exist(item.deletedBy);
+            should.exist(item.deletedAt);
+          });
+
+          // find (non-deleted) ProjectEstimationItems for project
+          return models.ProjectEstimationItem.findAllByProject(models, projectId, {
+            includeAllProjectEstimatinoItemsForInternalUsage: true,
+          });
+        }).then((entities) => {
+          entities.should.have.lengthOf(len, 'Number of created ProjectEstimationItems doesn\'t match');
+          if (len) {
+            entities[0].projectEstimationId.should.be.eql(estimation.id);
+            if (estimation.valueType === VALUE_TYPE.PERCENTAGE) {
+              entities[0].price.should.be.eql((estimation.price * estimation.value) / 100);
+            } else {
+              entities[0].price.should.be.eql(Number(estimation.value));
+            }
+            entities[0].type.should.be.eql(estimation.key.split('markup_')[1]);
+            entities[0].markupUsedReference.should.be.eql('projectSetting');
+            entities[0].markupUsedReferenceId.should.be.eql(id);
+            should.exist(entities[0].updatedAt);
+            should.not.exist(entities[0].deletedBy);
+            should.not.exist(entities[0].deletedAt);
+          }
+
+          next();
+        }).catch(next);
+      }
+    });
+};
+
+describe('CREATE Project Setting', () => {
+  let projectId;
+  let estimationId;
+
+  const body = {
+    key: 'markup_topcoder_service',
+    value: '3500',
+    valueType: 'double',
+    readPermission: {
+      projectRoles: ['customer'],
+      topcoderRoles: ['administrator'],
+    },
+    writePermission: {
+      allowRule: { topcoderRoles: ['administrator'] },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+  };
+
+  const estimation = {
+    buildingBlockKey: 'BLOCK_KEY',
+    conditions: '( HAS_DEV_DELIVERABLE && ONLY_ONE_OS_MOBILE && CA_NEEDED )',
+    price: 5000,
+    quantity: 10,
+    minTime: 35,
+    maxTime: 35,
+    metadata: {
+      deliverable: 'dev-qa',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        // Create projects
+        models.Project.create({
+          type: 'generic',
+          billingAccountId: 1,
+          name: 'test1',
+          description: 'test project1',
+          status: 'draft',
+          details: {},
+          createdBy: 1,
+          updatedBy: 1,
+          lastActivityAt: 1,
+          lastActivityUserId: '1',
+        })
+        .then((project) => {
+          projectId = project.id;
+
+          models.ProjectEstimation.create(_.assign(estimation, { projectId }))
+          .then((e) => {
+            estimationId = e.id;
+            done();
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('POST /projects/{projectId}/settings', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed project', (done) => {
+      request(server)
+        .post('/v5/projects/9999/settings')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(404, done);
+    });
+
+    it('should return 400 for missing key', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.key;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing value', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.value;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing valueType', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.valueType;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    xit('should return 400 for negative value when valueType = percentage', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      invalidBody.value = '-10';
+      invalidBody.valueType = VALUE_TYPE.PERCENTAGE;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    xit('should return 400 for value greater than 100 when valueType = percentage', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      invalidBody.value = '150';
+      invalidBody.valueType = VALUE_TYPE.PERCENTAGE;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400, for admin, when create key with existing key', (done) => {
+      const existing = _.cloneDeep(body);
+      existing.projectId = projectId;
+      existing.createdBy = 1;
+      existing.updatedBy = 1;
+
+      models.ProjectSetting.create(existing).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectId}/settings`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .send(body)
+          .expect(400, done);
+      }).catch(done);
+    });
+
+    it('should return 201 for manager with non-estimation type, not calculating project estimation items',
+      (done) => {
+        const createBody = _.cloneDeep(body);
+        createBody.key = 'markup_no_estimation';
+
+        request(server)
+          .post(`/v5/projects/${projectId}/settings`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(createBody)
+          .expect('Content-Type', /json/)
+          .expect(201)
+          .end((err, res) => {
+            if (err) done(err);
+
+            const resJson = res.body;
+            resJson.key.should.be.eql(createBody.key);
+            resJson.value.should.be.eql(createBody.value);
+            resJson.valueType.should.be.eql(createBody.valueType);
+            resJson.projectId.should.be.eql(projectId);
+            resJson.createdBy.should.be.eql(40051334);
+            should.exist(resJson.createdAt);
+            resJson.updatedBy.should.be.eql(40051334);
+            should.exist(resJson.updatedAt);
+            should.not.exist(resJson.deletedBy);
+            should.not.exist(resJson.deletedAt);
+            expectAfterCreate(resJson.id, projectId, null, 0, 0, err, done);
+          });
+      });
+
+    it('should return 201 for manager, calculating project estimation items', (done) => {
+      request(server)
+          .post(`/v5/projects/${projectId}/settings`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send(body)
+          .expect('Content-Type', /json/)
+          .expect(201)
+          .end((err, res) => {
+            if (err) done(err);
+
+            const resJson = res.body;
+            resJson.key.should.be.eql(body.key);
+            resJson.value.should.be.eql(body.value);
+            resJson.valueType.should.be.eql(body.valueType);
+            resJson.projectId.should.be.eql(projectId);
+            resJson.createdBy.should.be.eql(40051334);
+            should.exist(resJson.createdAt);
+            resJson.updatedBy.should.be.eql(40051334);
+            should.exist(resJson.updatedAt);
+            should.not.exist(resJson.deletedBy);
+            should.not.exist(resJson.deletedAt);
+            expectAfterCreate(resJson.id, projectId, _.assign(estimation, {
+              id: estimationId,
+              value: body.value,
+              valueType: body.valueType,
+              key: body.key,
+            }), 1, 0, err, done);
+          });
+    });
+
+    it('should return 201 for admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) done(err);
+
+          const resJson = res.body;
+          resJson.key.should.be.eql(body.key);
+          resJson.value.should.be.eql(body.value);
+          resJson.valueType.should.be.eql(body.valueType);
+          resJson.projectId.should.be.eql(projectId);
+          resJson.createdBy.should.be.eql(40051333);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(40051333);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+          done();
+        });
+    });
+
+    it('should return 201 for connect admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) done(err);
+
+          const resJson = res.body;
+          resJson.key.should.be.eql(body.key);
+          resJson.value.should.be.eql(body.value);
+          resJson.valueType.should.be.eql(body.valueType);
+          resJson.projectId.should.be.eql(projectId);
+          resJson.createdBy.should.be.eql(40051336);
+          resJson.updatedBy.should.be.eql(40051336);
+          should.exist(resJson.createdAt);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/projectSettings/delete.js b/src/routes/projectSettings/delete.js
new file mode 100644
index 0000000..aa85f49
--- /dev/null
+++ b/src/routes/projectSettings/delete.js
@@ -0,0 +1,63 @@
+/**
+ * API to delete a project setting
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('projectSetting.delete'),
+  (req, res, next) => {
+    const projectId = req.params.projectId;
+    const id = req.params.id;
+    let deletedEntity = null;
+
+    models.sequelize.transaction(() =>
+      models.ProjectSetting.findOne({
+        includeAllProjectSettingsForInternalUsage: true,
+        where: {
+          id,
+          projectId,
+        },
+      })
+      .then((entity) => {
+        // Not found
+        if (!entity) {
+          const apiErr = new Error(`Project setting not found for id ${id} and project id ${projectId}`);
+          apiErr.status = 404;
+          return Promise.reject(apiErr);
+        }
+
+        deletedEntity = entity;
+        // Update the deletedBy, then delete
+        return entity.update({ deletedBy: req.authUser.userId });
+      })
+      .then(entity => entity.destroy())
+      .then(() => {
+        // Calculate for valid estimation type
+        if (util.isProjectSettingForEstimation(deletedEntity.key)) {
+          req.log.debug(`Recalculate price breakdown for project id ${projectId}`);
+          return util.calculateProjectEstimationItems(req, projectId);
+        }
+
+        return Promise.resolve();
+      }),
+    ) // transaction end
+    .then(() => {
+      res.status(204).end();
+    })
+    .catch(next);
+  },
+];
diff --git a/src/routes/projectSettings/delete.spec.js b/src/routes/projectSettings/delete.spec.js
new file mode 100644
index 0000000..78d1e51
--- /dev/null
+++ b/src/routes/projectSettings/delete.spec.js
@@ -0,0 +1,281 @@
+/**
+ * Tests for delete.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import server from '../../app';
+import testUtil from '../../tests/util';
+import models from '../../models';
+
+const should = chai.should();
+
+const expectAfterDelete = (id, projectId, len, deletedLen, err, next) => {
+  if (err) throw err;
+
+  models.ProjectSetting.findOne({
+    includeAllProjectSettingsForInternalUsage: true,
+    where: {
+      id,
+      projectId,
+    },
+    paranoid: false,
+  })
+  .then((res) => {
+    if (!res) {
+      throw new Error('Should found the entity');
+    } else {
+      should.exist(res.deletedBy);
+      should.exist(res.deletedAt);
+
+      // find deleted ProjectEstimationItems for project
+      models.ProjectEstimationItem.findAllByProject(models, projectId, {
+        where: {
+          deletedAt: { $ne: null },
+        },
+        includeAllProjectEstimatinoItemsForInternalUsage: true,
+        paranoid: false,
+      }).then((items) => {
+        // deleted project estimation items
+        items.should.have.lengthOf(deletedLen, 'Number of deleted ProjectEstimationItems doesn\'t match');
+        _.each(items, (item) => {
+          should.exist(item.deletedBy);
+          should.exist(item.deletedAt);
+        });
+
+        // find (non-deleted) ProjectEstimationItems for project
+        return models.ProjectEstimationItem.findAllByProject(models, projectId, {
+          includeAllProjectEstimatinoItemsForInternalUsage: true,
+        });
+      }).then((items) => {
+        // all non-deleted project estimation item count
+        items.should.have.lengthOf(len, 'Number of created ProjectEstimationItems doesn\'t match');
+        next();
+      }).catch(next);
+    }
+  });
+};
+
+describe('DELETE Project Setting', () => {
+  let projectId;
+  let estimationId;
+  let id;
+  let id2;
+
+  const estimation = {
+    buildingBlockKey: 'BLOCK_KEY',
+    conditions: '( HAS_DEV_DELIVERABLE && SCREENS_COUNT_SMALL && CA_NEEDED)',
+    price: 6500.50,
+    quantity: 10,
+    minTime: 35,
+    maxTime: 35,
+    metadata: {
+      deliverable: 'dev-qa',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        // Create projects
+        models.Project.create({
+          type: 'generic',
+          billingAccountId: 1,
+          name: 'test1',
+          description: 'test project1',
+          status: 'draft',
+          details: {},
+          createdBy: 1,
+          updatedBy: 1,
+          lastActivityAt: 1,
+          lastActivityUserId: '1',
+        })
+        .then((project) => {
+          projectId = project.id;
+
+          models.ProjectSetting.bulkCreate([{
+            projectId,
+            key: 'markup_topcoder_service',
+            value: '5599.96',
+            valueType: 'double',
+            readPermission: {
+              projectRoles: ['customer'],
+              topcoderRoles: ['administrator'],
+            },
+            writePermission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['administrator'],
+              },
+              denyRule: {
+                projectRoles: ['copilot'],
+              },
+            },
+            createdBy: 1,
+            updatedBy: 1,
+          }, {
+            projectId,
+            key: 'markup_no_estimation',
+            value: '40',
+            valueType: 'percentage',
+            readPermission: {
+              topcoderRoles: ['administrator'],
+            },
+            writePermission: {
+              allowRule: { topcoderRoles: ['administrator'] },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            createdBy: 1,
+            updatedBy: 1,
+          }], { returning: true })
+          .then((settings) => {
+            id = settings[0].id;
+            id2 = settings[1].id;
+            models.ProjectEstimation.create(_.assign(estimation, { projectId }))
+            .then((e) => {
+              estimationId = e.id;
+              done();
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('DELETE /projects/{projectId}/settings/{id}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/settings/${id}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed project', (done) => {
+      request(server)
+        .delete(`/v5/projects/9999/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for non-existed project setting', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/settings/1234`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted project setting', (done) => {
+      models.ProjectSetting.destroy({ where: { id } })
+        .then(() => {
+          request(server)
+            .delete(`/v5/projects/${projectId}/settings/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        }).catch(done);
+    });
+
+    it('should return 204, for admin, if project setting was successfully removed', (done) => {
+      models.ProjectEstimationItem.create({
+        projectEstimationId: estimationId,
+        price: 1200,
+        type: 'topcoder_service',
+        markupUsedReference: 'projectSetting',
+        markupUsedReferenceId: id,
+        createdBy: 1,
+        updatedBy: 1,
+      })
+      .then(() => {
+        request(server)
+          .delete(`/v5/projects/${projectId}/settings/${id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(204)
+          .end(err => expectAfterDelete(id, projectId, 0, 1, err, done));
+      }).catch(done);
+    });
+
+    it('should return 204, for admin, if project setting with non-estimation type was successfully removed',
+    (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/settings/${id2}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(id2, projectId, 0, 0, err, done));
+    });
+
+    it('should return 204, for admin, another project setting exists if the project setting was successfully removed',
+    (done) => {
+      models.ProjectSetting.create({
+        projectId,
+        key: 'markup_fee',
+        value: '25',
+        valueType: 'percentage',
+        readPermission: {
+          projectRoles: ['customer'],
+          topcoderRoles: ['administrator'],
+        },
+        writePermission: {
+          allowRule: {
+            projectRoles: ['customer', 'copilot'],
+            topcoderRoles: ['administrator'],
+          },
+          denyRule: {
+            projectRoles: ['copilot'],
+          },
+        },
+        createdBy: 1,
+        updatedBy: 1,
+      }).then((anotherSetting) => {
+        models.ProjectEstimationItem.create({
+          projectEstimationId: estimationId,
+          price: 1200,
+          type: 'fee',
+          markupUsedReference: 'projectSetting',
+          markupUsedReferenceId: anotherSetting.id,
+          createdBy: 1,
+          updatedBy: 1,
+        }).then(() => {
+          request(server)
+            .delete(`/v5/projects/${projectId}/settings/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(204)
+            .end(err => expectAfterDelete(id, projectId, 1, 1, err, done));
+        });
+      }).catch(done);
+    });
+  });
+});
diff --git a/src/routes/projectSettings/list.js b/src/routes/projectSettings/list.js
new file mode 100644
index 0000000..480cd26
--- /dev/null
+++ b/src/routes/projectSettings/list.js
@@ -0,0 +1,50 @@
+/**
+ * API to list project setting
+ */
+import _ from 'lodash';
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  query: {
+    includeAllProjectSettingsForInternalUsage: Joi.boolean().optional(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('projectSetting.view'),
+  (req, res, next) => {
+    const projectId = req.params.projectId;
+    const options = {
+      where: {
+        projectId,
+      },
+      attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      raw: true,
+      // provide current user and project members list so `ProjectSetting.findAll` will return
+      // only records available to view by the current user
+      reqUser: req.authUser,
+      members: req.context.currentProjectMembers,
+    };
+
+    models.Project.findOne({ where: { id: projectId } })
+      .then((project) => {
+        if (!project) {
+          const apiErr = new Error(`Project not found for id ${projectId}`);
+          apiErr.status = 404;
+          return Promise.reject(apiErr);
+        }
+
+        return models.ProjectSetting.findAll(options);
+      })
+      .then((result) => {
+        res.json(_.filter(result, r => r));
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/projectSettings/list.spec.js b/src/routes/projectSettings/list.spec.js
new file mode 100644
index 0000000..f5e0b59
--- /dev/null
+++ b/src/routes/projectSettings/list.spec.js
@@ -0,0 +1,243 @@
+/**
+ * Tests for list.js
+ */
+import _ from 'lodash';
+import request from 'supertest';
+import chai from 'chai';
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('LIST Project Settings', () => {
+  let projectId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  const settings = [{
+    key: 'markup_topcoder_service',
+    value: '3500',
+    valueType: 'double',
+    readPermission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['administrator'],
+      },
+      denyRule: {
+        projectRoles: ['copilot'],
+        topcoderRoles: ['Connect Admin'],
+      },
+    },
+    writePermission: {
+      allowRule: { topcoderRoles: ['administrator'] },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  }, {
+    key: 'markup_fee',
+    value: '15',
+    valueType: 'percentage',
+    readPermission: {
+      topcoderRoles: ['administrator'],
+    },
+    writePermission: {
+      allowRule: { topcoderRoles: ['administrator'] },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  }];
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        // Create projects
+        models.Project.create({
+          type: 'generic',
+          billingAccountId: 1,
+          name: 'test1',
+          description: 'test project1',
+          status: 'draft',
+          details: {},
+          createdBy: 1,
+          updatedBy: 1,
+          lastActivityAt: 1,
+          lastActivityUserId: '1',
+        })
+        .then((project) => {
+          projectId = project.id;
+          // create members
+          models.ProjectMember.bulkCreate([{
+            id: 1,
+            userId: copilotUser.userId,
+            projectId,
+            role: 'copilot',
+            isPrimary: false,
+            createdBy: 1,
+            updatedBy: 1,
+          }, {
+            id: 2,
+            userId: memberUser.userId,
+            projectId,
+            role: 'customer',
+            isPrimary: true,
+            createdBy: 1,
+            updatedBy: 1,
+          }])
+          .then(() => {
+            models.ProjectSetting.bulkCreate(_.map(settings, s => _.assign(s, { projectId }))).then(() => done());
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/settings', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .expect(403, done);
+    });
+
+    it('should return 403 when user have no permission (non team member)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for deleted project', (done) => {
+      models.Project.destroy({ where: { id: projectId } })
+        .then(() => {
+          request(server)
+            .get(`/v5/projects/${projectId}/settings`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 404 for non-existed project', (done) => {
+      request(server)
+        .get('/v5/projects/99999/settings')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 0 setting when copilot has readPermission for both denyRule and allowRule', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(0);
+            done();
+          }
+        });
+    });
+
+    it('should return 0 setting when connect admin has readPermission for denyRule', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(0);
+            done();
+          }
+        });
+    });
+
+    it('should return 1 setting when user have readPermission (customer)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(1);
+            const setting = settings[0];
+            resJson[0].key.should.be.eql(setting.key);
+            resJson[0].value.should.be.eql(setting.value);
+            resJson[0].valueType.should.be.eql(setting.valueType);
+            resJson[0].projectId.should.be.eql(projectId);
+            resJson[0].readPermission.should.be.eql(setting.readPermission);
+            resJson[0].writePermission.should.be.eql(setting.writePermission);
+            done();
+          }
+        });
+    });
+
+    it('should return 2 settings when user have readPermission (administrator)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/settings`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(2);
+            const setting = settings[0];
+            resJson[0].key.should.be.eql(setting.key);
+            resJson[0].value.should.be.eql(setting.value);
+            resJson[0].valueType.should.be.eql(setting.valueType);
+            resJson[0].projectId.should.be.eql(projectId);
+            resJson[0].readPermission.should.be.eql(setting.readPermission);
+            resJson[0].writePermission.should.be.eql(setting.writePermission);
+            done();
+          }
+        });
+    });
+  });
+});
diff --git a/src/routes/projectSettings/update.js b/src/routes/projectSettings/update.js
new file mode 100644
index 0000000..9f22b1d
--- /dev/null
+++ b/src/routes/projectSettings/update.js
@@ -0,0 +1,74 @@
+/**
+ * API to update a project setting
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+import { VALUE_TYPE } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    value: Joi.string().max(255),
+    valueType: Joi.string().valid(_.values(VALUE_TYPE)),
+    projectId: Joi.any().strip(),
+    metadata: Joi.object(),
+    readPermission: Joi.object(),
+    writePermission: Joi.object(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('projectSetting.edit'),
+  (req, res, next) => {
+    let oldKey = null;
+    let updatedSetting = null;
+    const projectId = req.params.projectId;
+    const id = req.params.id;
+    const entityToUpdate = _.assign(req.body, {
+      updatedBy: req.authUser.userId,
+    });
+
+    models.sequelize.transaction(() =>
+      models.ProjectSetting.findOne({
+        includeAllProjectSettingsForInternalUsage: true,
+        where: {
+          id,
+          projectId,
+        },
+      })
+      .then((existing) => {
+        // Not found
+        if (!existing) {
+          const apiErr = new Error(`Project setting not found for id ${id} and project id ${projectId}`);
+          apiErr.status = 404;
+          return Promise.reject(apiErr);
+        }
+
+        oldKey = existing.key;
+        return existing.update(entityToUpdate);
+      })
+      .then((updated) => {
+        updatedSetting = updated;
+        if (util.isProjectSettingForEstimation(updatedSetting.key) || util.isProjectSettingForEstimation(oldKey)) {
+          req.log.debug(`Recalculate price breakdown for project id ${projectId}`);
+          return util.calculateProjectEstimationItems(req, projectId);
+        }
+        return Promise.resolve();
+      }),
+    ) // transaction end
+    .then(() => {
+      res.json(updatedSetting);
+    })
+    .catch(next);
+  },
+];
diff --git a/src/routes/projectSettings/update.spec.js b/src/routes/projectSettings/update.spec.js
new file mode 100644
index 0000000..916757f
--- /dev/null
+++ b/src/routes/projectSettings/update.spec.js
@@ -0,0 +1,408 @@
+/**
+ * Tests for update.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import server from '../../app';
+import testUtil from '../../tests/util';
+import models from '../../models';
+import { VALUE_TYPE } from '../../constants';
+
+const should = chai.should();
+
+const expectAfterUpdate = (id, projectId, estimation, len, deletedLen, err, next) => {
+  if (err) throw err;
+
+  models.ProjectSetting.findOne({
+    includeAllProjectSettingsForInternalUsage: true,
+    where: {
+      id,
+      projectId,
+    },
+  })
+  .then((res) => {
+    if (!res) {
+      throw new Error('Should found the entity');
+    } else {
+      // find deleted ProjectEstimationItems for project
+      models.ProjectEstimationItem.findAllByProject(models, projectId, {
+        where: {
+          deletedAt: { $ne: null },
+        },
+        includeAllProjectEstimatinoItemsForInternalUsage: true,
+        paranoid: false,
+      }).then((items) => {
+        // deleted project estimation items
+        items.should.have.lengthOf(deletedLen, 'Number of deleted ProjectEstimationItems doesn\'t match');
+
+        _.each(items, (item) => {
+          should.exist(item.deletedBy);
+          should.exist(item.deletedAt);
+        });
+
+        // find (non-deleted) ProjectEstimationItems for project
+        return models.ProjectEstimationItem.findAllByProject(models, projectId, {
+          includeAllProjectEstimatinoItemsForInternalUsage: true,
+        });
+      }).then((entities) => {
+        entities.should.have.lengthOf(len, 'Number of created ProjectEstimationItems doesn\'t match');
+        if (len) {
+          entities[0].projectEstimationId.should.be.eql(estimation.id);
+          if (estimation.valueType === VALUE_TYPE.PERCENTAGE) {
+            entities[0].price.should.be.eql((estimation.price * estimation.value) / 100);
+          } else {
+            entities[0].price.should.be.eql(Number(estimation.value));
+          }
+          entities[0].type.should.be.eql(estimation.key.split('markup_')[1]);
+          entities[0].markupUsedReference.should.be.eql('projectSetting');
+          entities[0].markupUsedReferenceId.should.be.eql(id);
+          should.exist(entities[0].updatedAt);
+          should.not.exist(entities[0].deletedBy);
+          should.not.exist(entities[0].deletedAt);
+        }
+
+        next();
+      });
+    }
+  });
+};
+
+describe('UPDATE Project Setting', () => {
+  let projectId;
+  let estimationId;
+  let id;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  const body = {
+    value: '5599.96',
+    valueType: 'double',
+    readPermission: {
+      projectRoles: ['customer'],
+      topcoderRoles: ['administrator'],
+    },
+    writePermission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['administrator', 'Connect Admin'],
+      },
+      denyRule: {
+        projectRoles: ['copilot'],
+        topcoderRoles: ['Connect Admin'],
+      },
+    },
+  };
+
+  // we don't include these params into the body, we cannot update them
+  // but we use them for creating model directly and for checking returned values
+  const bodyNonMutable = {
+    key: 'markup_topcoder_service',
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  const estimation = {
+    buildingBlockKey: 'BLOCK_KEY',
+    conditions: '( HAS_DEV_DELIVERABLE && ONLY_ONE_OS_MOBILE && CA_NEEDED )',
+    price: 6500.50,
+    quantity: 10,
+    minTime: 35,
+    maxTime: 35,
+    metadata: {
+      deliverable: 'dev-qa',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        // Create projects
+        models.Project.create({
+          type: 'generic',
+          billingAccountId: 1,
+          name: 'test1',
+          description: 'test project1',
+          status: 'draft',
+          details: {},
+          createdBy: 1,
+          updatedBy: 1,
+          lastActivityAt: 1,
+          lastActivityUserId: '1',
+        })
+        .then((project) => {
+          projectId = project.id;
+
+          models.ProjectMember.bulkCreate([{
+            id: 1,
+            userId: copilotUser.userId,
+            projectId,
+            role: 'copilot',
+            isPrimary: false,
+            createdBy: 1,
+            updatedBy: 1,
+          }, {
+            id: 2,
+            userId: memberUser.userId,
+            projectId,
+            role: 'customer',
+            isPrimary: true,
+            createdBy: 1,
+            updatedBy: 1,
+          }])
+          .then(() => {
+            models.ProjectSetting.create(_.assign({}, body, bodyNonMutable, {
+              projectId,
+            }))
+            .then((s) => {
+              id = s.id;
+
+              models.ProjectEstimation.create(_.assign(estimation, { projectId }))
+              .then((e) => {
+                estimationId = e.id;
+                done();
+              });
+            }).catch(done);
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH /projects/{projectId}/settings/{id}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 when user have no permission (non team member)', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 when copilot is in both denyRule and allowRule', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 when connect admin is in both denyRule and allowRule', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed project', (done) => {
+      request(server)
+        .patch(`/v5/projects/9999/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(404, done);
+    });
+
+    it('should return 404 for non-existed project setting', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/1234`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted project setting', (done) => {
+      models.ProjectSetting.destroy({ where: { id } })
+        .then(() => {
+          request(server)
+            .patch(`/v5/projects/${projectId}/settings/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(body)
+            .expect(404, done);
+        });
+    });
+
+    it('should return 400, when try to update key', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          key: 'updated_key',
+        })
+        .expect(400, done);
+    });
+
+    it('should return 200, for member with permission (team member), value updated but no project estimation present',
+    (done) => {
+      const notPresent = _.cloneDeep(body);
+      notPresent.value = '4500';
+
+      models.ProjectEstimation.destroy({
+        where: {
+          id: estimationId,
+        },
+      }).then(() => {
+        models.ProjectEstimationItem.destroy({
+          where: {
+            markupUsedReference: 'projectSetting',
+            markupUsedReferenceId: id,
+          },
+        }).then(() => {
+          request(server)
+            .patch(`/v5/projects/${projectId}/settings/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.member}`,
+            })
+            .send({
+              value: notPresent.value,
+            })
+            .expect('Content-Type', /json/)
+            .expect(200)
+            .end((err, res) => {
+              if (err) done(err);
+
+              const resJson = res.body;
+              resJson.id.should.be.eql(id);
+              resJson.key.should.be.eql(bodyNonMutable.key);
+              resJson.value.should.be.eql(notPresent.value);
+              resJson.valueType.should.be.eql(notPresent.valueType);
+              resJson.projectId.should.be.eql(projectId);
+              resJson.createdBy.should.be.eql(bodyNonMutable.createdBy);
+              resJson.updatedBy.should.be.eql(40051331);
+              should.exist(resJson.updatedAt);
+              should.not.exist(resJson.deletedBy);
+              should.not.exist(resJson.deletedAt);
+              expectAfterUpdate(id, projectId, _.assign(estimation, {
+                id: estimationId,
+                value: notPresent.value,
+                valueType: notPresent.valueType,
+                key: bodyNonMutable.key,
+              }), 0, 0, err, done);
+            });
+        });
+      }).catch(done);
+    });
+
+    it('should return 200 for admin when value updated, calculating project estimation items', (done) => {
+      body.value = '4500';
+
+      models.ProjectEstimationItem.create({
+        projectEstimationId: estimationId,
+        price: 1200,
+        type: 'topcoder_service',
+        markupUsedReference: 'projectSetting',
+        markupUsedReferenceId: id,
+        createdBy: 1,
+        updatedBy: 1,
+      }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/settings/${id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .send({
+            value: body.value,
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err, res) => {
+            if (err) done(err);
+
+            const resJson = res.body;
+            resJson.id.should.be.eql(id);
+            resJson.key.should.be.eql(bodyNonMutable.key);
+            resJson.value.should.be.eql(body.value);
+            resJson.valueType.should.be.eql(body.valueType);
+            resJson.projectId.should.be.eql(projectId);
+            resJson.createdBy.should.be.eql(bodyNonMutable.createdBy);
+            resJson.updatedBy.should.be.eql(40051333); // admin
+            should.exist(resJson.updatedAt);
+            should.not.exist(resJson.deletedBy);
+            should.not.exist(resJson.deletedAt);
+            expectAfterUpdate(id, projectId, _.assign(estimation, {
+              id: estimationId,
+              value: body.value,
+              valueType: body.valueType,
+              key: bodyNonMutable.key,
+            }), 1, 1, err, done);
+          });
+      }).catch(done);
+    });
+
+    it('should return 200, for admin, update valueType from double to percentage', (done) => {
+      body.value = '10.76';
+      body.valueType = VALUE_TYPE.PERCENTAGE;
+      request(server)
+        .patch(`/v5/projects/${projectId}/settings/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          value: body.value,
+          valueType: VALUE_TYPE.PERCENTAGE,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) done(err);
+
+          const resJson = res.body;
+          resJson.id.should.be.eql(id);
+          resJson.key.should.be.eql(bodyNonMutable.key);
+          resJson.value.should.be.eql(body.value);
+          resJson.valueType.should.be.eql(VALUE_TYPE.PERCENTAGE);
+          resJson.projectId.should.be.eql(projectId);
+          resJson.createdBy.should.be.eql(bodyNonMutable.createdBy);
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+          expectAfterUpdate(id, projectId, _.assign(estimation, {
+            id: estimationId,
+            value: body.value,
+            valueType: body.valueType,
+            key: bodyNonMutable.key,
+          }), 1, 0, err, done);
+        });
+    });
+  });
+});
diff --git a/src/routes/projectTemplates/list.js b/src/routes/projectTemplates/list.js
index f278c2e..81d1ee4 100644
--- a/src/routes/projectTemplates/list.js
+++ b/src/routes/projectTemplates/list.js
@@ -10,13 +10,24 @@ const permissions = tcMiddleware.permissions;
 module.exports = [
   permissions('projectTemplate.view'),
   (req, res, next) => {
-    util.fetchFromES('projectTemplates')
+    util.fetchFromES('projectTemplates', {
+      query: {
+        nested: {
+          path: 'projectTemplates',
+          query: {
+            match: { 'projectTemplates.disabled': false },
+          },
+          inner_hits: {},
+        },
+      },
+    }, 'metadata')
     .then((data) => {
       if (data.projectTemplates.length === 0) {
         req.log.debug('No projectTemplate found in ES');
         models.ProjectTemplate.findAll({
           where: {
             deletedAt: { $eq: null },
+            disabled: false,
           },
           attributes: { exclude: ['deletedAt', 'deletedBy'] },
           raw: true,
diff --git a/src/routes/projectTemplates/list.spec.js b/src/routes/projectTemplates/list.spec.js
index 02a6a4b..25eaae9 100644
--- a/src/routes/projectTemplates/list.spec.js
+++ b/src/routes/projectTemplates/list.spec.js
@@ -20,7 +20,7 @@ describe('LIST project templates', () => {
       question: 'question 1',
       info: 'info 1',
       aliases: ['key-1', 'key_1'],
-      disabled: true,
+      disabled: false,
       hidden: true,
       scope: {
         scope1: {
diff --git a/src/routes/projectTypes/create.js b/src/routes/projectTypes/create.js
index 5f2f6fd..88051a5 100644
--- a/src/routes/projectTypes/create.js
+++ b/src/routes/projectTypes/create.js
@@ -41,10 +41,10 @@ module.exports = [
     });
 
     // Check if duplicated key
-    return models.ProjectType.findByPk(req.body.key)
+    return models.ProjectType.findByPk(req.body.key, { paranoid: false })
       .then((existing) => {
         if (existing) {
-          const apiErr = new Error(`Project type already exists for key ${req.params.key}`);
+          const apiErr = new Error(`Project type already exists (may be deleted) for key ${req.body.key}`);
           apiErr.status = 400;
           return Promise.reject(apiErr);
         }
diff --git a/src/routes/projectTypes/list.spec.js b/src/routes/projectTypes/list.spec.js
index 05700bd..7074ae8 100644
--- a/src/routes/projectTypes/list.spec.js
+++ b/src/routes/projectTypes/list.spec.js
@@ -40,6 +40,9 @@ describe('LIST project types', () => {
     },
   ];
 
+  before((done) => {
+    testUtil.clearES(done);
+  });
   beforeEach((done) => {
     testUtil.clearDb()
       .then(() => models.ProjectType.create(types[0]))
diff --git a/src/routes/projectUpgrade/create.js b/src/routes/projectUpgrade/create.js
index a1fe2fc..1037f00 100644
--- a/src/routes/projectUpgrade/create.js
+++ b/src/routes/projectUpgrade/create.js
@@ -45,9 +45,9 @@ async function findCompletedProjectEndDate(projectId, transaction) {
  */
 function applyTemplate(template, source, destination) {
   if (!template || typeof template !== 'object') { return; }
-  if (!template.questions || !template.questions.length) { return; }
+  if (!template.sections || !template.sections.length) { return; }
   // questions field is actually array of sections
-  const templateQuestions = template.questions;
+  const templateQuestions = template.sections;
   // loop through for every section
   templateQuestions.forEach((section) => {
     // find subsections
@@ -208,6 +208,7 @@ async function migrateFromV2ToV3(req, project, defaultProductTemplateId, phaseNa
 
   req.app.emit(EVENT.ROUTING_KEY.PROJECT_UPDATED, {
     req,
+    original: previousValue,
     updated: _.assign({ resource: RESOURCES.PROJECT }, project.toJSON()),
   });
 }
diff --git a/src/routes/projectUpgrade/create.spec.js b/src/routes/projectUpgrade/create.spec.js
index 9ce5641..c0a1b68 100644
--- a/src/routes/projectUpgrade/create.spec.js
+++ b/src/routes/projectUpgrade/create.spec.js
@@ -107,7 +107,7 @@ describe('Project upgrade', () => {
           alias2: [1, 2, 3],
         },
         template: {
-          questions: [
+          sections: [
             {
               subSections: [
                 { fieldName: 'details.name' },
diff --git a/src/routes/projects/create.js b/src/routes/projects/create.js
index 40278d2..9e5214d 100644
--- a/src/routes/projects/create.js
+++ b/src/routes/projects/create.js
@@ -8,7 +8,7 @@ import moment from 'moment';
 
 import models from '../../models';
 import { PROJECT_MEMBER_ROLE, MANAGER_ROLES, PROJECT_STATUS, PROJECT_PHASE_STATUS,
-  EVENT, RESOURCES, REGEX } from '../../constants';
+  EVENT, RESOURCES, REGEX, WORKSTREAM_STATUS } from '../../constants';
 import fieldLookupValidation from '../../middlewares/fieldLookupValidation';
 import util from '../../util';
 
@@ -24,7 +24,7 @@ const traverse = require('traverse');
  */
 const permissions = require('tc-core-library-js').middleware.permissions;
 
-const createProjectValdiations = {
+const createProjectValidations = {
   body: Joi.object().keys({
     name: Joi.string().required(),
     description: Joi.string().allow(null).allow('').optional(),
@@ -37,6 +37,10 @@ const createProjectValdiations = {
     bookmarks: Joi.array().items(Joi.object().keys({
       title: Joi.string(),
       address: Joi.string().regex(REGEX.URL),
+      createdAt: Joi.date(),
+      createdBy: Joi.number().integer().positive(),
+      updatedAt: Joi.date(),
+      updatedBy: Joi.number().integer().positive(),
     })).optional().allow(null),
     estimatedPrice: Joi.number().precision(2).positive().optional()
         .allow(null),
@@ -64,18 +68,117 @@ const createProjectValdiations = {
       buildingBlockKey: Joi.string().required(),
       metadata: Joi.object().optional(),
     })).optional(),
+    attachments: Joi.array().items(Joi.object().keys({
+      category: Joi.string().required(),
+      contentType: Joi.string().required(),
+      description: Joi.string().allow(null).allow('').optional(),
+      filePath: Joi.string().required(),
+      size: Joi.number().required(),
+      title: Joi.string().required(),
+    })).optional(),
   }).required(),
 };
 
+/**
+ * Create ProjectEstimationItem with BuildingBlock.
+ * @param {Array} estimations the project estimations
+ * @param {Number} userId the request user id
+ * @returns {Promise} the promise that resolves to the created ProjectEstimationItem
+ */
+function createEstimationItemsWithBuildingBlock(estimations, userId) {
+  const buildingBlockKeys = _.map(estimations, estimation => estimation.buildingBlockKey);
+  // get all building blocks
+  return models.BuildingBlock.findAll({
+    where: { deletedAt: { $eq: null }, key: buildingBlockKeys },
+    raw: true,
+    includePrivateConfigForInternalUsage: true,
+  }).then((buildingBlocks) => {
+    const blocks = {};
+    _.forEach(buildingBlocks, (block) => {
+      if (block) {
+        blocks[block.key] = block;
+      }
+    });
+    const estimationItems = [];
+    _.forEach(estimations, (estimation) => {
+      const block = blocks[estimation.buildingBlockKey];
+      if (block && _.get(block, 'privateConfig.priceItems')) {
+        _.forOwn(block.privateConfig.priceItems, (item, key) => {
+          let itemPrice;
+          if (_.isString(item) && item.endsWith('%')) {
+            const percent = _.toNumber(item.replace('%', '')) / 100;
+            itemPrice = _.toNumber(estimation.price) * percent;
+          } else {
+            itemPrice = item;
+          }
+          estimationItems.push({
+            projectEstimationId: estimation.id,
+            price: itemPrice,
+            type: key,
+            markupUsedReference: 'buildingBlock',
+            markupUsedReferenceId: block.id,
+            createdBy: userId,
+            updatedBy: userId,
+          });
+        });
+      }
+    });
+
+    return models.ProjectEstimationItem.bulkCreate(estimationItems, { returning: true });
+  });
+}
+
+/**
+ * Create workstreams for newly created project based on provided workstreams config
+ * and project details
+ *
+ * @param {Object} req               express request object
+ * @param {Object} newProject        new created project
+ * @param {Object} workstreamsConfig config of workstreams to create
+ *
+ * @returns {Promise} the list of created WorkStreams
+ */
+function createWorkstreams(req, newProject, workstreamsConfig) {
+  if (!workstreamsConfig) {
+    req.log.debug('no workstream config found');
+    return Promise.resolve([]);
+  }
+
+  req.log.debug('creating project workstreams');
+
+  // get value of the field in the project data which would determine which workstream types to create
+  const projectFieldValue = _.get(newProject, workstreamsConfig.projectFieldName);
+
+  // the list of workstream types to create, based on the project field values
+  // mapping provided in `workstreamTypesToProjectValues`
+  const workstreamTypesToCreate = _.keys(_.pickBy(workstreamsConfig.workstreamTypesToProjectValues, fieldValues => (
+    _.intersection(fieldValues, projectFieldValue).length > 0
+  )));
+
+  // the list workstreams to create
+  const workstreamsToCreate = _.filter(workstreamsConfig.workstreams, workstream => (
+    _.includes(workstreamTypesToCreate, workstream.type)
+  )).map(workstreamToCreate => _.assign({}, workstreamToCreate, {
+    projectId: newProject.id,
+    status: WORKSTREAM_STATUS.DRAFT,
+    createdBy: req.authUser.userId,
+    updatedBy: req.authUser.userId,
+  }));
+
+  return models.WorkStream.bulkCreate(workstreamsToCreate);
+  // return Promise.resolve(workstreamsToCreate);
+}
+
 /**
  * Create the project, project phases and products. This needs to be done before creating direct project.
  * @param {Object} req the request
  * @param {Object} project the project
  * @param {Object} projectTemplate the project template
- * @param {Array} productTemplates array of the templates of the products used in the projec template
+ * @param {Array}  productTemplates array of the templates of the products used in the project template
+ * @param {Array}  phasesList list phases definitions to create
  * @returns {Promise} the promise that resolves to the created project and phases
  */
-function createProjectAndPhases(req, project, projectTemplate, productTemplates) {
+function createProjectAndPhases(req, project, projectTemplate, productTemplates, phasesList) {
   const result = {
     newProject: null,
     newPhases: [],
@@ -103,63 +206,94 @@ function createProjectAndPhases(req, project, projectTemplate, productTemplates)
     }
     return Promise.resolve(newProject);
   }).then((newProject) => {
+    req.log.debug('creating project estimation items with building blocks');
+    if (result.estimations && result.estimations.length > 0) {
+      return createEstimationItemsWithBuildingBlock(result.estimations, req.authUser.userId)
+      .then((estimationItems) => {
+        req.log.debug(`creating ${estimationItems.length} project estimation items`);
+        // ignore project estimation items for now
+        return Promise.resolve(newProject);
+      });
+    }
+    return Promise.resolve(newProject);
+  }).then((newProject) => {
+    if (project.attachments && (project.attachments.length > 0)) {
+      req.log.debug('creating project attachments');
+      const attachments = project.attachments.map(attachment => Object.assign({
+        projectId: newProject.id,
+        createdBy: req.authUser.userId,
+        updatedBy: req.authUser.userId,
+      }, attachment));
+      return models.ProjectAttachment.bulkCreate(attachments, { returning: true }).then((projectAttachments) => {
+        result.attachments = _.map(projectAttachments, attachment =>
+          _.omit(attachment.toJSON(), ['deletedAt', 'deletedBy']));
+        return Promise.resolve(newProject);
+      });
+    }
+    return Promise.resolve(newProject);
+  })
+  .then((newProject) => {
     result.newProject = newProject;
 
     // backward compatibility for releasing the service before releasing the front end
     if (!projectTemplate) {
       return Promise.resolve(result);
     }
-    const phases = _.filter(_.values(projectTemplate.phases), p => !!p);
     const productTemplateMap = {};
     productTemplates.forEach((pt) => {
       productTemplateMap[pt.id] = pt;
     });
-    return Promise.all(_.map(phases, (phase, phaseIdx) => {
-      const duration = _.get(phase, 'duration', 1);
-      const startDate = moment.utc().hours(0).minutes(0).seconds(0)
-        .milliseconds(0);
-      // Create phase
-      return models.ProjectPhase.create({
-        projectId: newProject.id,
-        name: _.get(phase, 'name', `Stage ${phaseIdx}`),
-        duration,
-        startDate: startDate.format(),
-        endDate: moment.utc(startDate).add(duration - 1, 'days').format(),
-        status: _.get(phase, 'status', PROJECT_PHASE_STATUS.DRAFT),
-        budget: _.get(phase, 'budget', 0),
-        updatedBy: req.authUser.userId,
-        createdBy: req.authUser.userId,
-      }).then((newPhase) => {
-        req.log.debug(`Creating products in the newly created phase ${newPhase.id}`);
-        // Create products
-        return models.PhaseProduct.bulkCreate(_.map(phase.products, (product, productIndex) => ({
-          phaseId: newPhase.id,
+
+    if (phasesList) {
+      return Promise.all(_.map(phasesList, (phase, phaseIdx) => {
+        const duration = _.get(phase, 'duration', 1);
+        const startDate = moment.utc().hours(0).minutes(0).seconds(0)
+          .milliseconds(0);
+        // Create phase
+        return models.ProjectPhase.create({
           projectId: newProject.id,
-          estimatedPrice: _.get(product, 'estimatedPrice', 0),
-          name: _.get(product, 'name', _.get(productTemplateMap, `${product.id}.name`, `Product ${productIndex}`)),
-          // assumes that phase template always contains id of each product
-          templateId: parseInt(product.id, 10),
+          name: _.get(phase, 'name', `Stage ${phaseIdx}`),
+          duration,
+          startDate: startDate.format(),
+          endDate: moment.utc(startDate).add(duration - 1, 'days').format(),
+          status: _.get(phase, 'status', PROJECT_PHASE_STATUS.DRAFT),
+          budget: _.get(phase, 'budget', 0),
           updatedBy: req.authUser.userId,
           createdBy: req.authUser.userId,
-        })), { returning: true })
-        .then((products) => {
-          // Add phases and products to the project JSON, so they can be stored to ES later
-          const newPhaseJson = _.omit(newPhase.toJSON(), ['deletedAt', 'deletedBy']);
-          newPhaseJson.products = _.map(products, product =>
-            _.omit(product.toJSON(), ['deletedAt', 'deletedBy']));
-          result.newPhases.push(newPhaseJson);
-          return Promise.resolve();
+        }).then((newPhase) => {
+          req.log.debug(`Creating products in the newly created phase ${newPhase.id}`);
+          // Create products
+          return models.PhaseProduct.bulkCreate(_.map(phase.products, (product, productIndex) => ({
+            phaseId: newPhase.id,
+            projectId: newProject.id,
+            estimatedPrice: _.get(product, 'estimatedPrice', 0),
+            name: _.get(product, 'name', _.get(productTemplateMap, `${product.id}.name`, `Product ${productIndex}`)),
+            // assumes that phase template always contains id of each product
+            templateId: parseInt(product.id, 10),
+            updatedBy: req.authUser.userId,
+            createdBy: req.authUser.userId,
+          })), { returning: true })
+          .then((products) => {
+            // Add phases and products to the project JSON, so they can be stored to ES later
+            const newPhaseJson = _.omit(newPhase.toJSON(), ['deletedAt', 'deletedBy']);
+            newPhaseJson.products = _.map(products, product =>
+              _.omit(product.toJSON(), ['deletedAt', 'deletedBy']));
+            result.newPhases.push(newPhaseJson);
+            return Promise.resolve();
+          });
         });
-      });
-    }));
-  }).then(() => Promise.resolve(result));
+      }));
+    }
+    return Promise.resolve();
+  })
+  .then(() => Promise.resolve(result));
 }
 
 /**
  * Validates the project and product templates for the give project template id.
  *
  * @param {Integer} templateId id of the project template which should be validated
- * @returns {Promise} the promise that resolves to an object containing validated project and product templates
+ * @returns {Promise} the promise that resolves to an object containing validated project, product templates and phases list
  */
 function validateAndFetchTemplates(templateId) {
   // backward compatibility for releasing the service before releasing the front end
@@ -176,40 +310,72 @@ function validateAndFetchTemplates(templateId) {
     return Promise.resolve(existingProjectTemplate);
   })
   .then((projectTemplate) => {
-    const phases = _.values(projectTemplate.phases);
+    // for old projectTemplate with `phases` just get phases config directly from projectTemplate
+    if (projectTemplate.phases) {
+      // for now support both ways: creating phases and creating workstreams
+      const phasesList = _(projectTemplate.phases).omit('workstreamsConfig').values().value();
+      const workstreamsConfig = _.get(projectTemplate.phases, 'workstreamsConfig');
+
+      return { projectTemplate, phasesList, workstreamsConfig };
+    }
+
+    // for new projectTemplates try to get phases from the `planConfig`, if it's defined
+    if (projectTemplate.planConfig) {
+      return models.PlanConfig.findOneWithLatestRevision(projectTemplate.planConfig).then((planConfig) => {
+        if (!planConfig) {
+          const apiErr = new Error(`Cannot find planConfig ${JSON.stringify(projectTemplate.planConfig)}`);
+          apiErr.status = 400;
+          throw apiErr;
+        }
+
+        // for now support both ways: creating phases and creating workstreams
+        const phasesList = _(planConfig.config).omit('workstreamsConfig').values().value();
+        const workstreamsConfig = _.get(planConfig.config, 'workstreamsConfig');
+
+        return { projectTemplate, phasesList, workstreamsConfig };
+      });
+    }
+
+    return { projectTemplate };
+  })
+  .then(({ projectTemplate, phasesList, workstreamsConfig }) => {
     const productPromises = [];
-    phases.forEach((phase) => {
-      // Make sure number of products of per phase <= max value
-      const productCount = _.isArray(phase.products) ? phase.products.length : 0;
-      if (productCount > config.maxPhaseProductCount) {
-        const apiErr = new Error(`Number of products per phase cannot exceed ${config.maxPhaseProductCount}`);
-        apiErr.status = 400;
-        throw apiErr;
-      }
-      _.map(phase.products, (product) => {
-        productPromises.push(models.ProductTemplate.findByPk(product.id)
-        .then((productTemplate) => {
-          if (!productTemplate) {
-            // Not found
-            const apiErr = new Error(`Product template not found for id ${product.id}`);
-            apiErr.status = 400;
-            return Promise.reject(apiErr);
-          }
-          return Promise.resolve(productTemplate);
-        }));
+    if (phasesList) {
+      phasesList.forEach((phase) => {
+        // Make sure number of products of per phase <= max value
+        const productCount = _.isArray(phase.products) ? phase.products.length : 0;
+        if (productCount > config.maxPhaseProductCount) {
+          const apiErr = new Error(`Number of products per phase cannot exceed ${config.maxPhaseProductCount}`);
+          apiErr.status = 400;
+          throw apiErr;
+        }
+        _.map(phase.products, (product) => {
+          productPromises.push(models.ProductTemplate.findByPk(product.id)
+          .then((productTemplate) => {
+            if (!productTemplate) {
+              // Not found
+              const apiErr = new Error(`Product template not found for id ${product.id}`);
+              apiErr.status = 400;
+              return Promise.reject(apiErr);
+            }
+            return Promise.resolve(productTemplate);
+          }));
+        });
       });
-    });
+    }
     if (productPromises.length > 0) {
-      return Promise.all(productPromises).then(productTemplates => ({ projectTemplate, productTemplates }));
+      return Promise.all(productPromises).then(productTemplates => (
+        { projectTemplate, productTemplates, phasesList, workstreamsConfig }
+      ));
     }
     // if there is no phase or product in a phase is specified, return empty product templates
-    return Promise.resolve({ projectTemplate, productTemplates: [] });
+    return Promise.resolve({ projectTemplate, productTemplates: [], phasesList, workstreamsConfig });
   });
 }
 
 module.exports = [
   // handles request validations
-  validate(createProjectValdiations),
+  validate(createProjectValidations),
   permissions('project.create'),
   fieldLookupValidation(models.ProjectType, 'key', 'body.type', 'Project type'),
   /**
@@ -260,19 +426,30 @@ module.exports = [
     let newProject = null;
     let newPhases;
     let projectEstimations;
+    let projectAttachments;
     models.sequelize.transaction(() => {
       req.log.debug('Create Project - Starting transaction');
       // Validate the templates
       return validateAndFetchTemplates(project.templateId)
       // Create project and phases
-      .then(({ projectTemplate, productTemplates }) => {
+      .then(({ projectTemplate, productTemplates, phasesList, workstreamsConfig }) => {
         req.log.debug('Creating project, phase and products');
-        return createProjectAndPhases(req, project, projectTemplate, productTemplates);
+        // only if workstream config is provided, treat such project as using workstreams
+        // otherwise project would still use phases
+        if (workstreamsConfig) {
+          _.set(project, 'details.settings.workstreams', true);
+        }
+        return createProjectAndPhases(req, project, projectTemplate, productTemplates, phasesList)
+          .then(createdProjectAndPhases =>
+            createWorkstreams(req, createdProjectAndPhases.newProject, workstreamsConfig)
+              .then(() => createdProjectAndPhases),
+          );
       })
       .then((createdProjectAndPhases) => {
         newProject = createdProjectAndPhases.newProject;
         newPhases = createdProjectAndPhases.newPhases;
         projectEstimations = createdProjectAndPhases.estimations;
+        projectAttachments = createdProjectAndPhases.attachments;
 
         req.log.debug('new project created (id# %d, name: %s)', newProject.id, newProject.name);
         // create direct project with name and description
@@ -286,21 +463,22 @@ module.exports = [
         }
         req.log.debug('creating project history for project %d', newProject.id);
         // add to project history asynchronously, don't wait for it to complete
-        return models.ProjectHistory.create({
+        models.ProjectHistory.create({
           projectId: newProject.id,
           status: PROJECT_STATUS.DRAFT,
           cancelReason: null,
           updatedBy: req.authUser.userId,
         }).then(() => req.log.debug('project history created for project %d', newProject.id))
           .catch(() => req.log.error('project history failed for project %d', newProject.id));
+        return Promise.resolve();
       });
     })
     .then(() => {
       newProject = newProject.get({ plain: true });
       // remove utm details & deletedAt field
       newProject = _.omit(newProject, ['deletedAt', 'utm']);
-      // add an empty attachments array
-      newProject.attachments = [];
+      // add the project attachments, if any
+      newProject.attachments = projectAttachments;
       // set phases array
       newProject.phases = newPhases;
       // sets estimations array
diff --git a/src/routes/projects/create.spec.js b/src/routes/projects/create.spec.js
index a8e0a0f..0249784 100644
--- a/src/routes/projects/create.spec.js
+++ b/src/routes/projects/create.spec.js
@@ -157,6 +157,96 @@ describe('Project create', () => {
           createdBy: 1,
           updatedBy: 2,
         },
+        {
+          id: 4,
+          name: 'template with workstreams',
+          key: 'key 3',
+          category: 'category 3',
+          icon: 'http://example.com/icon3.ico',
+          question: 'question 3',
+          info: 'info 3',
+          aliases: [],
+          scope: {},
+          phases: {
+            workstreamsConfig: {
+              projectFieldName: 'details.appDefinition.deliverables',
+              workstreamTypesToProjectValues: {
+                development: [
+                  'dev-qa',
+                ],
+                design: [
+                  'design',
+                ],
+                deployment: [
+                  'deployment',
+                ],
+                qa: [
+                  'dev-qa',
+                ],
+              },
+              workstreams: [
+                {
+                  name: 'Design Workstream',
+                  type: 'design',
+                },
+                {
+                  name: 'Development Workstream',
+                  type: 'development',
+                },
+                {
+                  name: 'QA Workstream',
+                  type: 'qa',
+                },
+                {
+                  name: 'Deployment Workstream',
+                  typ: 'deployment',
+                },
+              ],
+            },
+          },
+          createdBy: 1,
+          updatedBy: 2,
+        },
+      ]))
+      .then(() => models.BuildingBlock.bulkCreate([
+        {
+          id: 1,
+          key: 'BLOCK_KEY',
+          config: {},
+          privateConfig: {
+            priceItems: {
+              community: 3456,
+              topcoder_service: '19%',
+              fee: 1234,
+            },
+          },
+          createdBy: 1,
+          updatedBy: 2,
+        },
+        {
+          id: 2,
+          key: 'BLOCK_KEY2',
+          config: {},
+          privateConfig: {
+            message: 'invalid config',
+          },
+          createdBy: 1,
+          updatedBy: 2,
+        },
+        {
+          id: 3,
+          key: 'BLOCK_KEY3',
+          config: {},
+          privateConfig: {
+            priceItems: {
+              community: '34%',
+              topcoder_service: 6789,
+              fee: '56%',
+            },
+          },
+          createdBy: 1,
+          updatedBy: 2,
+        },
       ]))
       .then(() => done());
   });
@@ -277,31 +367,6 @@ describe('Project create', () => {
         .expect(400, done);
     });
 
-    it('should return 201 if error to create direct project', (done) => {
-      const validBody = _.cloneDeep(body);
-      validBody.templateId = 3;
-      const mockHttpClient = _.merge(testUtil.mockHttpClient, {
-        post: () => Promise.reject(new Error('error message')),
-      });
-      sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
-      request(server)
-        .post('/v5/projects')
-        .set({
-          Authorization: `Bearer ${testUtil.jwts.member}`,
-        })
-        .send(validBody)
-        .expect('Content-Type', /json/)
-        .expect(201)
-        .end((err) => {
-          if (err) {
-            done(err);
-          } else {
-            server.services.pubsub.publish.calledWith('project.draft-created').should.be.true;
-            done();
-          }
-        });
-    });
-
     it('should return 201 if valid user and data', (done) => {
       const validBody = _.cloneDeep(body);
       validBody.templateId = 3;
@@ -478,6 +543,80 @@ describe('Project create', () => {
         });
     });
 
+    it('should create project with workstreams if template has them defined', (done) => {
+      const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+        post: () => Promise.resolve({
+          status: 200,
+          data: {
+            id: 'requesterId',
+            version: 'v3',
+            result: {
+              success: true,
+              status: 200,
+              content: {
+                projectId: 128,
+              },
+            },
+          },
+        }),
+      });
+      sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
+      request(server)
+        .post('/v5/projects')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(_.merge({
+          templateId: 4,
+          details: {
+            appDefinition: {
+              deliverables: ['dev-qa', 'design'],
+            },
+          },
+        }, body))
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            should.exist(resJson.billingAccountId);
+            should.exist(resJson.name);
+            resJson.status.should.be.eql('draft');
+            resJson.type.should.be.eql(body.type);
+            resJson.members.should.have.lengthOf(1);
+            resJson.members[0].role.should.be.eql('customer');
+            resJson.members[0].userId.should.be.eql(40051331);
+            resJson.members[0].projectId.should.be.eql(resJson.id);
+            resJson.members[0].isPrimary.should.be.truthy;
+            resJson.bookmarks.should.have.lengthOf(1);
+            resJson.bookmarks[0].title.should.be.eql('title1');
+            resJson.bookmarks[0].address.should.be.eql('http://www.address.com');
+            resJson.phases.should.have.lengthOf(0);
+            server.services.pubsub.publish.calledWith('project.draft-created').should.be.true;
+
+            // verify that project has been marked to use workstreams
+            resJson.details.settings.workstreams.should.be.true;
+
+            // Check Workstreams records are created correctly
+            models.WorkStream.findAll({
+              where: {
+                projectId: resJson.id,
+              },
+              raw: true,
+            }).then((workStreams) => {
+              workStreams.length.should.be.eql(3);
+              _.filter(workStreams, { type: 'development', name: 'Development Workstream' }).length.should.be.eql(1);
+              _.filter(workStreams, { type: 'design', name: 'Design Workstream' }).length.should.be.eql(1);
+              _.filter(workStreams, { type: 'qa', name: 'QA Workstream' }).length.should.be.eql(1);
+              done();
+            }).catch(done);
+          }
+        });
+    });
+
     it('should return 201 if valid user and data (with estimation)', (done) => {
       const validBody = _.cloneDeep(body);
       validBody.estimation = [
@@ -609,7 +748,7 @@ describe('Project create', () => {
               projectEstimations[0].metadata.deliverable.should.be.eql('design');
               projectEstimations[0].buildingBlockKey.should.be.eql('ZEPLIN_APP_ADDON_CA');
               done();
-            });
+            }).catch(done);
           }
         });
     });
@@ -697,5 +836,112 @@ describe('Project create', () => {
           }
         });
     });
+
+    it('should create correct estimation items with estimation', (done) => {
+      const validBody = _.cloneDeep(body);
+      validBody.estimation = [
+        {
+          conditions: '( HAS_DEV_DELIVERABLE && (ONLY_ONE_OS_MOBILE) )',
+          price: 1000,
+          minTime: 2,
+          maxTime: 2,
+          metadata: {},
+          buildingBlockKey: 'BLOCK_KEY',
+        },
+        {
+          conditions: '( HAS_DEV_DELIVERABLE && (ONLY_ONE_OS_MOBILE) )',
+          price: 1000,
+          minTime: 2,
+          maxTime: 2,
+          metadata: {},
+          buildingBlockKey: 'BLOCK_KEY2',
+        },
+        {
+          conditions: '( HAS_DEV_DELIVERABLE && (ONLY_ONE_OS_MOBILE) )',
+          price: 1000,
+          minTime: 2,
+          maxTime: 2,
+          metadata: {},
+          buildingBlockKey: 'BLOCK_KEY3',
+        },
+      ];
+      validBody.templateId = 3;
+      const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+        post: () => Promise.resolve({
+          status: 200,
+          data: {
+            projectId: 128,
+          },
+        }),
+      });
+      sandbox.stub(util, 'getHttpClient', () => mockHttpClient);
+      request(server)
+        .post('/v5/projects')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(validBody)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            should.exist(resJson.name);
+            should.exist(resJson.estimations);
+            resJson.estimations.length.should.be.eql(3);
+
+            const totalPromises = [];
+            // check estimation items one by one
+            _.forEach(resJson.estimations, estimation => models.ProjectEstimationItem.findAll({
+              where: {
+                projectEstimationId: estimation.id,
+              },
+              raw: true,
+            }).then((items) => {
+              totalPromises.concat(_.map(items, (item) => {
+                should.exist(item.type);
+                should.exist(item.price);
+                should.exist(item.markupUsedReference);
+                should.exist(item.markupUsedReferenceId);
+
+                item.markupUsedReference.should.be.eql('buildingBlock');
+                if (estimation.buildingBlockKey === 'BLOCK_KEY') {
+                  if (item.type === 'community') {
+                    item.price.should.be.eql(3456);
+                  } else if (item.type === 'topcoder_service') {
+                    item.price.should.be.eql(190);
+                  } else if (item.type === 'fee') {
+                    item.price.should.be.eql(1234);
+                  } else {
+                    return Promise.reject('estimation item type is not correct');
+                  }
+                } else if (estimation.buildingBlockKey === 'BLOCK_KEY2') {
+                  return Promise.reject('should not create estimation item for invalid building block');
+                } else if (estimation.buildingBlockKey === 'BLOCK_KEY3') {
+                  if (item.type === 'community') {
+                    item.price.should.be.eql(340);
+                  } else if (item.type === 'topcoder_service') {
+                    item.price.should.be.eql(6789);
+                  } else if (item.type === 'fee') {
+                    item.price.should.be.eql(560);
+                  } else {
+                    return Promise.reject('estimation item type is not correct');
+                  }
+                } else {
+                  return Promise.reject('estimation building block key is not correct');
+                }
+                return Promise.resolve();
+              }));
+            }));
+
+            Promise.all(totalPromises).then(() => {
+              done();
+            }).catch(e => done(e));
+          }
+        });
+    });
   });
 });
diff --git a/src/routes/projects/delete.spec.js b/src/routes/projects/delete.spec.js
index 9a9bc3d..5a4447d 100644
--- a/src/routes/projects/delete.spec.js
+++ b/src/routes/projects/delete.spec.js
@@ -19,8 +19,6 @@ const expectAfterDelete = (id, err, next) => {
       if (!res) {
         throw new Error('Should found the entity');
       } else {
-        server.services.pubsub.publish.calledWith('project.deleted').should.be.true;
-
         chai.assert.isNotNull(res.deletedAt);
         chai.assert.isNotNull(res.deletedBy);
 
@@ -29,7 +27,8 @@ const expectAfterDelete = (id, err, next) => {
           .set({
             Authorization: `Bearer ${testUtil.jwts.admin}`,
           })
-          .expect(404, next);
+          .expect(404)
+          .end(next);
       }
     }), 500);
 };
@@ -110,7 +109,8 @@ describe('Project delete test', () => {
         .set({
           Authorization: `Bearer ${testUtil.jwts.copilot}`,
         })
-        .expect(403, done);
+        .expect(403)
+        .end(done);
     });
 
     it('should return 204 if project was successfully removed', (done) => {
diff --git a/src/routes/projects/get.js b/src/routes/projects/get.js
index 770f611..3169a0d 100644
--- a/src/routes/projects/get.js
+++ b/src/routes/projects/get.js
@@ -68,6 +68,10 @@ module.exports = [
       })
       .then((invites) => {
         project.invites = invites;
+        return models.ScopeChangeRequest.getProjectScopeChangeRequests(projectId);
+      })
+      .then((scopeChangeRequests) => {
+        project.scopeChangeRequests = scopeChangeRequests;
         res.status(200).json(project);
       })
       .catch(err => next(err));
diff --git a/src/routes/projects/get.spec.js b/src/routes/projects/get.spec.js
index c697605..2731d0a 100644
--- a/src/routes/projects/get.spec.js
+++ b/src/routes/projects/get.spec.js
@@ -76,7 +76,8 @@ describe('GET Project', () => {
     it('should return 403 if user is not authenticated', (done) => {
       request(server)
           .get(`/v5/projects/${project2.id}`)
-          .expect(403, done);
+          .expect(403)
+          .end(done);
     });
 
     it('should return 404 if requested project doesn\'t exist', (done) => {
@@ -85,16 +86,18 @@ describe('GET Project', () => {
           .set({
             Authorization: `Bearer ${testUtil.jwts.admin}`,
           })
-          .expect(404, done);
+          .expect(404)
+          .end(done);
     });
 
-    it('should return 404 if user does not have access to the project', (done) => {
+    it('should return 403 if user does not have access to the project', (done) => {
       request(server)
           .get(`/v5/projects/${project2.id}`)
           .set({
             Authorization: `Bearer ${testUtil.jwts.member}`,
           })
-          .expect(403, done);
+          .expect(403)
+          .end(done);
     });
 
     it('should return the project when registerd member attempts to access the project', (done) => {
diff --git a/src/routes/projects/list.js b/src/routes/projects/list.js
old mode 100644
new mode 100755
diff --git a/src/routes/projects/list.spec.js b/src/routes/projects/list.spec.js
index 5c826f1..7b1f00e 100644
--- a/src/routes/projects/list.spec.js
+++ b/src/routes/projects/list.spec.js
@@ -2,7 +2,7 @@
 /* eslint-disable max-len */
 import chai from 'chai';
 import request from 'supertest';
-import sleep from 'sleep';
+// import sleep from 'sleep';
 import config from 'config';
 import models from '../../models';
 import server from '../../app';
@@ -126,6 +126,7 @@ describe('LIST Project', () => {
   before(function inner(done) {
     this.timeout(10000);
     testUtil.clearDb()
+      .then(() => testUtil.clearES())
       .then(() => {
         const p1 = models.Project.create({
           type: 'generic',
@@ -246,8 +247,9 @@ describe('LIST Project', () => {
           return Promise.all([esp1, esp2, esp3]);
         }).then(() => {
           // sleep for some time, let elasticsearch indices be settled
-          sleep.sleep(5);
-          done();
+          // sleep.sleep(5);
+          testUtil.wait(done);
+          // done();
         });
       });
   });
diff --git a/src/routes/projects/update.js b/src/routes/projects/update.js
index 20beea4..6418bf1 100644
--- a/src/routes/projects/update.js
+++ b/src/routes/projects/update.js
@@ -56,6 +56,10 @@ const updateProjectValdiations = {
     bookmarks: Joi.array().items(Joi.object().keys({
       title: Joi.string(),
       address: Joi.string().regex(REGEX.URL),
+      createdAt: Joi.date(),
+      createdBy: Joi.number().integer().positive(),
+      updatedAt: Joi.date(),
+      updatedBy: Joi.number().integer().positive(),
     })).optional().allow(null),
     type: Joi.string().max(45),
     details: Joi.any(),
@@ -79,13 +83,53 @@ const updateProjectValdiations = {
   }),
 };
 
+/**
+ * Gets scopechange fields either from
+ * "template.scope" (for old templates) or from "form.scope" (for new templates).
+ *
+ * @param {Object} project The project object
+ *
+ * @returns {Array} - the scopeChangeFields
+ */
+const getScopeChangeFields = (project) => {
+  const scopeChangeFields = _.get(project, 'template.scope.scopeChangeFields');
+  const getFromForm = _project => _.get(_project, 'template.form.config.scopeChangeFields');
+
+  return scopeChangeFields || getFromForm(project);
+};
+
+const isScopeUpdated = (existingProject, updatedProps) => {
+  const scopeFields = getScopeChangeFields(existingProject);
+
+  if (scopeFields) {
+    for (let idx = 0; idx < scopeFields.length; idx += 1) {
+      const field = scopeFields[idx];
+      const oldFieldValue = _.get(existingProject, field);
+      const updateFieldValue = _.get(updatedProps, field);
+      if (oldFieldValue !== updateFieldValue) {
+        return true;
+      }
+    }
+  }
+  return false;
+};
+
 // NOTE- decided to disable all additional checks for now.
 const validateUpdates = (existingProject, updatedProps, req) => {
   const errors = [];
   switch (existingProject.status) {
     case PROJECT_STATUS.COMPLETED:
-      errors.push(`cannot update a project that is in ${existingProject.status}' state`);
+      errors.push(`cannot update a project that is in '${existingProject.status}' state`);
       break;
+    case PROJECT_STATUS.REVIEWED:
+    case PROJECT_STATUS.ACTIVE:
+    case PROJECT_STATUS.PAUSED: {
+      if (isScopeUpdated(existingProject, updatedProps)) {
+        // TODO commented to disable the scope change flow for immediate release
+        // errors.push(`Scope changes are not allowed for '${existingProject.status}' project`);
+      }
+      break;
+    }
     default:
       break;
     // disabling this check for now.
@@ -150,14 +194,20 @@ module.exports = [
       lock: { of: models.Project },
     })
       .then((_prj) => {
-        project = _prj;
-        if (!project) {
+        if (!_prj) {
           // handle 404
           const err = new Error(`project not found for id ${projectId}`);
           err.status = 404;
           return Promise.reject(err);
         }
+        if (!_prj.templateId) return Promise.resolve({ _prj });
+        return models.ProjectTemplate.getTemplate(_prj.templateId)
+        .then(template => Promise.resolve({ _prj, template }));
+      })
+      .then(({ _prj, template }) => {
+        project = _prj;
         previousValue = _.clone(project.get({ plain: true }));
+        previousValue.template = template;
         // run additional validations
         const validationErrors = validateUpdates(previousValue, updatedProps, req);
         if (validationErrors.length > 0) {
@@ -173,7 +223,9 @@ module.exports = [
         const members = req.context.currentProjectMembers;
         const validRoles = [
           PROJECT_MEMBER_ROLE.MANAGER,
-          PROJECT_MEMBER_ROLE.MANAGER,
+          PROJECT_MEMBER_ROLE.PROGRAM_MANAGER,
+          PROJECT_MEMBER_ROLE.PROJECT_MANAGER,
+          PROJECT_MEMBER_ROLE.SOLUTION_ARCHITECT,
         ].map(x => x.toLowerCase());
         const matchRole = role => _.indexOf(validRoles, role.toLowerCase()) >= 0;
         if (updatedProps.status === PROJECT_STATUS.ACTIVE &&
@@ -228,6 +280,7 @@ module.exports = [
         );
         req.app.emit(EVENT.ROUTING_KEY.PROJECT_UPDATED, {
           req,
+          original: previousValue,
           updated: _.assign({ resource: RESOURCES.PROJECT }, project),
         });
 
diff --git a/src/routes/projects/update.spec.js b/src/routes/projects/update.spec.js
index 78ebe96..ea49063 100644
--- a/src/routes/projects/update.spec.js
+++ b/src/routes/projects/update.spec.js
@@ -12,6 +12,7 @@ import busApi from '../../services/busApi';
 import {
   PROJECT_STATUS,
   BUS_API_EVENT,
+  CONNECT_NOTIFICATION_EVENT,
 } from '../../constants';
 
 const should = chai.should();
@@ -745,7 +746,7 @@ describe('Project', () => {
         createEventSpy = sandbox.spy(busApi, 'createEvent');
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project status update', (done) => {
+      it('should send correct BUS API messages when project status updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -753,7 +754,6 @@ describe('Project', () => {
         })
         .send({
           status: PROJECT_STATUS.COMPLETED,
-
         })
         .expect(200)
         .end((err) => {
@@ -761,14 +761,32 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
+              createEventSpy.callCount.should.equal(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                status: PROJECT_STATUS.COMPLETED,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_COMPLETED).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project details update', (done) => {
+      it('should send correct BUS API messages when project details updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -785,22 +803,32 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ resource: 'project' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ id: project1.id })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ details: { info: 'something' } })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ updatedBy: testUtil.userIds.admin })).should.be.true;
+              createEventSpy.callCount.should.equal(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                details: { info: 'something' },
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_SPECIFICATION_MODIFIED).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project name update', (done) => {
+      it('should send correct BUS API messages when project name updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -808,7 +836,6 @@ describe('Project', () => {
         })
         .send({
           name: 'New project name',
-
         })
         .expect(200)
         .end((err) => {
@@ -816,22 +843,32 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ resource: 'project' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ id: project1.id })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ name: 'New project name' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ updatedBy: testUtil.userIds.admin })).should.be.true;
+              createEventSpy.callCount.should.equal(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                name: 'New project name',
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_SPECIFICATION_MODIFIED).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: 'New project name',
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project description update', (done) => {
+      it('should send correct BUS API messages when project description updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -839,7 +876,6 @@ describe('Project', () => {
         })
         .send({
           description: 'Updated description',
-
         })
         .expect(200)
         .end((err) => {
@@ -847,22 +883,32 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ resource: 'project' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ id: project1.id })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                 sinon.match({ description: 'Updated description' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                 sinon.match({ updatedBy: testUtil.userIds.admin })).should.be.true;
+              createEventSpy.callCount.should.equal(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                description: 'Updated description',
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_SPECIFICATION_MODIFIED).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('sends single BUS_API_EVENT.PROJECT_UPDATED message on project bookmarks update', (done) => {
+      it('should send correct BUS API messages when project bookmarks updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -880,22 +926,32 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.calledOnce.should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ resource: 'project' })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ id: project1.id })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ bookmarks: [{ title: 'title1', address: 'http://someurl.com' }] })).should.be.true;
-              createEventSpy.firstCall.calledWith(BUS_API_EVENT.PROJECT_UPDATED,
-                sinon.match({ updatedBy: testUtil.userIds.admin })).should.be.true;
+              createEventSpy.callCount.should.equal(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                bookmarks: [{ title: 'title1', address: 'http://someurl.com' }],
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_LINK_CREATED).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_UPDATED, sinon.match({
+                projectId: project1.id,
+                projectName: project1.name,
+                projectUrl: `https://local.topcoder-dev.com/projects/${project1.id}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('should send BUS_API_EVENT.PROJECT_UPDATED message when project estimatedPrice is updated', (done) => {
+      it('should send correct BUS API messages when project estimatedPrice updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -903,7 +959,6 @@ describe('Project', () => {
         })
         .send({
           estimatedPrice: 123,
-
         })
         .expect(200)
         .end((err) => {
@@ -911,14 +966,23 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.called.should.be.true;
+              createEventSpy.callCount.should.equal(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                // FIXME https://github.com/sequelize/sequelize/issues/8019
+                // estimatedPrice: 123,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('should send BUS_API_EVENT.PROJECT_UPDATED message when project actualPrice is updated', (done) => {
+      it('should send correct BUS API messages when project actualPrice updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -926,7 +990,6 @@ describe('Project', () => {
         })
         .send({
           actualPrice: 123,
-
         })
         .expect(200)
         .end((err) => {
@@ -934,14 +997,23 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.called.should.be.true;
+              createEventSpy.callCount.should.equal(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                // FIXME https://github.com/sequelize/sequelize/issues/8019
+                // actualPrice: 123,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
               done();
             });
           }
         });
       });
 
-      it('should send BUS_API_EVENT.PROJECT_UPDATED message when project terms are updated', (done) => {
+      it('should send correct BUS API messages when project terms are updated', (done) => {
         request(server)
         .patch(`/v5/projects/${project1.id}`)
         .set({
@@ -949,7 +1021,6 @@ describe('Project', () => {
         })
         .send({
           terms: [1, 2, 3],
-
         })
         .expect(200)
         .end((err) => {
@@ -957,7 +1028,15 @@ describe('Project', () => {
             done(err);
           } else {
             testUtil.wait(() => {
-              createEventSpy.called.should.be.true;
+              createEventSpy.callCount.should.equal(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_UPDATED, sinon.match({
+                resource: 'project',
+                id: project1.id,
+                terms: [1, 2, 3],
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
               done();
             });
           }
diff --git a/src/routes/scopeChangeRequests/create.js b/src/routes/scopeChangeRequests/create.js
new file mode 100644
index 0000000..e63caae
--- /dev/null
+++ b/src/routes/scopeChangeRequests/create.js
@@ -0,0 +1,85 @@
+import _ from 'lodash';
+import Joi from 'joi';
+import validate from 'express-validation';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import { SCOPE_CHANGE_REQ_STATUS, PROJECT_MEMBER_ROLE, PROJECT_STATUS } from '../../constants';
+import models from '../../models';
+
+/**
+ * API to add a scope change request for a project.
+ */
+const permissions = tcMiddleware.permissions;
+
+const createScopeChangeRequestValidations = {
+  body: {
+    oldScope: Joi.object(),
+    newScope: Joi.object(),
+  },
+};
+
+module.exports = [
+  // handles request validations
+  validate(createScopeChangeRequestValidations),
+  permissions('project.edit'),
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const oldScope = _.get(req, 'body.oldScope');
+    const newScope = _.get(req, 'body.newScope');
+    const members = req.context.currentProjectMembers;
+    const isCustomer = !_.isUndefined(_.find(members,
+        m => m.userId === req.authUser.userId && m.role === PROJECT_MEMBER_ROLE.CUSTOMER));
+
+    const scopeChange = {
+      oldScope,
+      newScope,
+      status: isCustomer ? SCOPE_CHANGE_REQ_STATUS.APPROVED : SCOPE_CHANGE_REQ_STATUS.PENDING,
+      projectId,
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+    };
+
+    return models.Project.findOne({
+      where: { id: projectId },
+    })
+
+    .then((project) => {
+      if (!project) {
+        const err = new Error(`Project with id ${projectId} not found`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+
+      // If the project is not frozen yet, the changes can be saved directly into projects db.
+      // Scope change request workflow is not required.
+      const statusesForNonFrozenProjects = [PROJECT_STATUS.DRAFT, PROJECT_STATUS.IN_REVIEW];
+      if (statusesForNonFrozenProjects.indexOf(project.status) > -1) {
+        const err = new Error(
+          `Cannot create a scope change request for projects with statuses: ${
+            statusesForNonFrozenProjects.join(', ')}`);
+        err.status = 403;
+        return Promise.reject(err);
+      }
+
+      return models.ScopeChangeRequest.findPendingScopeChangeRequest(projectId);
+    })
+
+    .then((pendingScopeChangeReq) => {
+      if (pendingScopeChangeReq) {
+        const err = new Error('Cannot create a new scope change request while there is a pending request');
+        err.status = 403;
+        return Promise.reject(err);
+      }
+
+      req.log.debug('creating scope change request');
+      return models.ScopeChangeRequest.create(scopeChange);
+    })
+
+    .then((_newScopeChange) => {
+      req.log.debug('Created scope change request');
+      res.json(_newScopeChange);
+      return Promise.resolve();
+    })
+
+    .catch(err => next(err));
+  },
+];
diff --git a/src/routes/scopeChangeRequests/create.spec.js b/src/routes/scopeChangeRequests/create.spec.js
new file mode 100644
index 0000000..ca15f65
--- /dev/null
+++ b/src/routes/scopeChangeRequests/create.spec.js
@@ -0,0 +1,280 @@
+import sinon from 'sinon';
+import request from 'supertest';
+import _ from 'lodash';
+import Promise from 'bluebird';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+import { PROJECT_STATUS, PROJECT_MEMBER_ROLE, SCOPE_CHANGE_REQ_STATUS } from '../../constants';
+
+/**
+ * Creates a project with given status
+ * @param {string} status - Status of the project
+ *
+ * @returns {Promise} - promise for project creation
+ */
+function createProject(status) {
+  const newMember = (userId, role, project) => ({
+    userId,
+    projectId: project.id,
+    role,
+    isPrimary: true,
+    createdBy: 1,
+    updatedBy: 1,
+  });
+
+  return models.Project.create({
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status,
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  }).then(project =>
+    Promise.all([
+      models.ProjectMember.create(newMember(testUtil.userIds.member, PROJECT_MEMBER_ROLE.CUSTOMER, project)),
+      models.ProjectMember.create(newMember(testUtil.userIds.manager, PROJECT_MEMBER_ROLE.MANAGER, project)),
+    ]).then(() => project),
+  );
+}
+
+/**
+ * creates a new scope change request object
+ * @returns {Object} - scope change request object
+ */
+function newScopeChangeRequest() {
+  return {
+    newScope: {
+      appDefinition: {
+        numberScreens: '5-8',
+      },
+    },
+    oldScope: {
+      appDefinition: {
+        numberScreens: '2-4',
+      },
+    },
+  };
+}
+
+/**
+ * Asserts the status of the Scope change request
+ * @param {Object} response - Response object from the post service
+ * @param {string} expectedStatus - Expected status of the Scope Change Request
+ *
+ * @returns {undefined} - throws error if assertion failed
+ */
+function assertStatus(response, expectedStatus) {
+  const status = _.get(response, 'body.status');
+  sinon.assert.match(status, expectedStatus);
+}
+
+/**
+ * Updaes the status of scope change requests for the given project in db
+ * @param {Object} project - the project
+ * @param {string} status - the new status for update
+ *
+ * @returns {Promise} the promise to update the status
+ */
+function updateScopeChangeStatuses(project, status) {
+  return models.ScopeChangeRequest.update({ status }, { where: { projectId: project.id } });
+}
+
+
+describe('Create Scope Change Rquest', () => {
+  let projects;
+  let projectWithPendingChange;
+  let projectWithApprovedChange;
+
+  before((done) => {
+    const projectStatuses = [
+      PROJECT_STATUS.DRAFT,
+      PROJECT_STATUS.IN_REVIEW,
+      PROJECT_STATUS.REVIEWED,
+      PROJECT_STATUS.ACTIVE,
+    ];
+
+    Promise.all(projectStatuses.map(status => createProject(status)))
+      .then(_projects => _projects.map((project, i) => [projectStatuses[i], project]))
+      .then((_projectStatusPairs) => {
+        projects = _.fromPairs(_projectStatusPairs);
+      })
+      .then(() => done());
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('POST projects/{projectId}/scopeChangeRequests', () => {
+    it('Should create scope change request for project in reviewed status', (done) => {
+      const project = projects[PROJECT_STATUS.REVIEWED];
+
+      request(server)
+        .post(`/v5/projects/${project.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            projectWithPendingChange = project;
+
+            assertStatus(res, SCOPE_CHANGE_REQ_STATUS.PENDING);
+            done();
+          }
+        });
+    });
+
+    it('Should create scope change request for project in active status', (done) => {
+      const project = projects[PROJECT_STATUS.ACTIVE];
+
+      request(server)
+        .post(`/v5/projects/${project.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            projectWithApprovedChange = project;
+
+            assertStatus(res, SCOPE_CHANGE_REQ_STATUS.APPROVED);
+            done();
+          }
+        });
+    });
+
+    it('Should return error with status 403 if project is in draft status', (done) => {
+      const project = projects[PROJECT_STATUS.DRAFT];
+
+      request(server)
+        .post(`/v5/projects/${project.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(403)
+        .end(err => done(err));
+    });
+
+    it('Should return error with status 403 if project is in in_review status', (done) => {
+      const project = projects[PROJECT_STATUS.IN_REVIEW];
+
+      request(server)
+        .post(`/v5/projects/${project.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(403)
+        .end(err => done(err));
+    });
+
+    it('Should return error with status 404 if project not present', (done) => {
+      const nonExistentProjectId = 341212;
+      request(server)
+        .post(`/v5/projects/${nonExistentProjectId}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(404)
+        .end(err => done(err));
+    });
+
+    it('Should return error with status 403 if there is a request in pending status', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectWithPendingChange.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(403)
+        .end(err => done(err));
+    });
+
+    it('Should return error with status 403 if there is a request in approved status', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectWithApprovedChange.id}/scopeChangeRequests`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(newScopeChangeRequest())
+        .expect(403)
+        .end(err => done(err));
+    });
+
+    it('Should create scope change request if there is a request in canceled status', (done) => {
+      updateScopeChangeStatuses(projectWithApprovedChange, SCOPE_CHANGE_REQ_STATUS.CANCELED).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectWithApprovedChange.id}/scopeChangeRequests`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send(newScopeChangeRequest())
+          .expect(200)
+          .end((err, res) => {
+            if (err) {
+              done(err);
+            } else {
+              assertStatus(res, SCOPE_CHANGE_REQ_STATUS.APPROVED);
+              done();
+            }
+          });
+      });
+    });
+
+    it('Should create scope change request if there is a request in rejected status', (done) => {
+      updateScopeChangeStatuses(projectWithApprovedChange, SCOPE_CHANGE_REQ_STATUS.REJECTED).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectWithApprovedChange.id}/scopeChangeRequests`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send(newScopeChangeRequest())
+          .expect(200)
+          .end((err, res) => {
+            if (err) {
+              done(err);
+            } else {
+              assertStatus(res, SCOPE_CHANGE_REQ_STATUS.APPROVED);
+              done();
+            }
+          });
+      });
+    });
+
+    it('Should create scope change request if there is a request in activated status', (done) => {
+      updateScopeChangeStatuses(projectWithApprovedChange, SCOPE_CHANGE_REQ_STATUS.ACTIVATED).then(() => {
+        request(server)
+          .post(`/v5/projects/${projectWithApprovedChange.id}/scopeChangeRequests`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send(newScopeChangeRequest())
+          .expect(200)
+          .end((err, res) => {
+            if (err) {
+              done(err);
+            } else {
+              assertStatus(res, SCOPE_CHANGE_REQ_STATUS.APPROVED);
+              done();
+            }
+          });
+      });
+    });
+  });
+});
diff --git a/src/routes/scopeChangeRequests/update.js b/src/routes/scopeChangeRequests/update.js
new file mode 100644
index 0000000..1f1e8ea
--- /dev/null
+++ b/src/routes/scopeChangeRequests/update.js
@@ -0,0 +1,127 @@
+import _ from 'lodash';
+import Joi from 'joi';
+import validate from 'express-validation';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import {
+    SCOPE_CHANGE_REQ_STATUS,
+    PROJECT_MEMBER_ROLE,
+    USER_ROLE,
+    PROJECT_MEMBER_MANAGER_ROLES,
+    EVENT,
+} from '../../constants';
+import models from '../../models';
+
+/**
+ * API to add a scope change request for a project.
+ */
+const permissions = tcMiddleware.permissions;
+
+const updateScopeChangeRequestValidations = {
+  body: {
+    status: Joi.string().valid(_.values(SCOPE_CHANGE_REQ_STATUS)),
+  },
+};
+
+/**
+ * Merges the new scope that's being activated into the details json of the project and updates the db
+ * @param {Object} req The request object
+ * @param {Object} newScope The new scope to apply
+ * @param {string} projectId The project id
+ *
+ * @returns {Promise} The promise to update the project with merged data
+ */
+function updateProjectDetails(req, newScope, projectId) {
+  return models.Project.findByPk(projectId).then((project) => {
+    const previousValue = _.clone(project.get({ plain: true }));
+
+    if (!project) {
+      const err = new Error('Project not found');
+      err.status = 404;
+      return Promise.reject(err);
+    }
+
+    const updatedDetails = _.mergeWith(
+      {}, project.details, newScope,
+      (_objValue, srcValue) => {
+        if (_.isArray(srcValue)) {
+          return srcValue;
+        }
+        return undefined;
+      });
+
+    return project.update({ details: updatedDetails }).then((updatedProject) => {
+      const updated = updatedProject.get({ plain: true });
+      const original = _.omit(previousValue, ['deletedAt', 'deletedBy']);
+
+      // publish original and updated project data
+      req.app.services.pubsub.publish(
+        EVENT.ROUTING_KEY.PROJECT_UPDATED,
+        { original, updated },
+        { correlationId: req.id },
+      );
+      req.app.emit(EVENT.ROUTING_KEY.PROJECT_UPDATED, { req, original, updated });
+
+      return updatedProject;
+    });
+  });
+}
+
+module.exports = [
+  // handles request validations
+  validate(updateScopeChangeRequestValidations),
+  permissions('project.edit'),
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const requestId = _.parseInt(req.params.requestId);
+    const updatedProps = req.body;
+    const members = req.context.currentProjectMembers;
+    const member = _.find(members, m => m.userId === req.authUser.userId);
+    const isCustomer = member && member.role === PROJECT_MEMBER_ROLE.CUSTOMER;
+    // const isCopilot = member && member.role === PROJECT_MEMBER_ROLE.COPILOT;
+    const isManager = member && PROJECT_MEMBER_MANAGER_ROLES.indexOf(member.role) > -1;
+    const isAdmin = util.hasRoles(req, [USER_ROLE.CONNECT_ADMIN, USER_ROLE.TOPCODER_ADMIN]);
+
+    req.log.debug('finding scope change', requestId);
+    return models.ScopeChangeRequest.findScopeChangeRequest(projectId, { requestId })
+    .then((scopeChangeReq) => {
+      // req.log.debug(scopeChangeReq);
+      if (!scopeChangeReq) {
+        const err = new Error('Scope change request does not exist');
+        err.status = 404;
+        return next(err);
+      }
+      const statusesForCustomers = [SCOPE_CHANGE_REQ_STATUS.APPROVED, SCOPE_CHANGE_REQ_STATUS.REJECTED];
+      if (statusesForCustomers.indexOf(updatedProps.status) > -1 && !isCustomer && !isAdmin) {
+        const err = new Error('Only customer can approve the request');
+        err.status = 401;
+        return next(err);
+      }
+      const statusesForManagers = [SCOPE_CHANGE_REQ_STATUS.ACTIVATED];
+      if (statusesForManagers.indexOf(updatedProps.status) > -1 && !isManager && !isAdmin) {
+        const err = new Error('Only managers can activate the request');
+        err.status = 401;
+        return next(err);
+      }
+      const statusesForSelf = [SCOPE_CHANGE_REQ_STATUS.CANCELED];
+      const isSelf = scopeChangeReq.createdBy === req.authUser.userId;
+      if (statusesForSelf.indexOf(updatedProps.status) > -1 && !isSelf && !isAdmin) {
+        const err = new Error('One can cancel only own requests');
+        err.status = 401;
+        return next(err);
+      }
+
+      return (
+        updatedProps.status === SCOPE_CHANGE_REQ_STATUS.ACTIVATED
+          ? updateProjectDetails(req, scopeChangeReq.newScope, projectId)
+          : Promise.resolve()
+      )
+      .then(() => scopeChangeReq.update(updatedProps))
+      .then((_updatedReq) => {
+        res.json(_updatedReq);
+        return Promise.resolve();
+      });
+    })
+    .catch(err => next(err));
+  },
+];
diff --git a/src/routes/scopeChangeRequests/update.spec.js b/src/routes/scopeChangeRequests/update.spec.js
new file mode 100644
index 0000000..cbceca5
--- /dev/null
+++ b/src/routes/scopeChangeRequests/update.spec.js
@@ -0,0 +1,213 @@
+import sinon from 'sinon';
+import request from 'supertest';
+import _ from 'lodash';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+import { PROJECT_STATUS, PROJECT_MEMBER_ROLE, SCOPE_CHANGE_REQ_STATUS } from '../../constants';
+
+/**
+ * Creates a project with given status
+ * @param {string} status - Status of the project
+ *
+ * @returns {Promise} - promise for project creation
+ */
+function createProject(status) {
+  const newMember = (userId, role, project) => ({
+    userId,
+    projectId: project.id,
+    role,
+    isPrimary: true,
+    createdBy: 1,
+    updatedBy: 1,
+  });
+
+  return models.Project.create({
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status,
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  }).then(project =>
+    Promise.all([
+      models.ProjectMember.create(newMember(testUtil.userIds.member, PROJECT_MEMBER_ROLE.CUSTOMER, project)),
+      models.ProjectMember.create(newMember(testUtil.userIds.manager, PROJECT_MEMBER_ROLE.MANAGER, project)),
+    ]).then(() => project),
+  );
+}
+
+/**
+ * Asserts the status of the Scope change request
+ * @param {Object} updatedScopeChangeRequest - the updated scope change request from db
+ * @param {string} expectedStatus - Expected status of the Scope Change Request
+ *
+ * @returns {undefined} - throws error if assertion failed
+ */
+function assertStatus(updatedScopeChangeRequest, expectedStatus) {
+  sinon.assert.match(updatedScopeChangeRequest.status, expectedStatus);
+}
+
+/**
+ * create scope change request for the given project
+ * @param {Object} project - the project
+ *
+ * @returns {Promise} - the promise to create scope change request
+ */
+function createScopeChangeRequest(project) {
+  return models.ScopeChangeRequest.create({
+    newScope: {
+      appDefinition: {
+        numberScreens: '5-8',
+      },
+    },
+    oldScope: {
+      appDefinition: {
+        numberScreens: '2-4',
+      },
+    },
+    projectId: project.id,
+    status: SCOPE_CHANGE_REQ_STATUS.PENDING,
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  });
+}
+
+/**
+ * Updates the details json of the project
+ * @param {string} projectId The project id
+ * @param {Object} detailsChange The changes to be merged with details json
+ *
+ * @returns {Promise} A promise to update details json in the project
+ */
+function updateProjectDetails(projectId, detailsChange) {
+  return models.Project.findByPk(projectId).then((project) => {
+    const updatedDetails = _.merge({}, project.details, detailsChange);
+    return project.update({ details: updatedDetails });
+  });
+}
+
+describe('Update Scope Change Rquest', () => {
+  let project;
+  let scopeChangeRequest;
+
+  before((done) => {
+    testUtil
+      .clearDb()
+      .then(() => createProject(PROJECT_STATUS.REVIEWED))
+      .then((_project) => {
+        project = _project;
+        return project;
+      })
+      .then(_project => createScopeChangeRequest(_project))
+      .then((_scopeChangeRequest) => {
+        scopeChangeRequest = _scopeChangeRequest;
+        return scopeChangeRequest;
+      })
+      .then(() => done());
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH projects/{projectId}/scopeChangeRequests/{requestId}', () => {
+    it('Should approve change request with customer login', (done) => {
+      request(server)
+        .patch(`/v5/projects/${project.id}/scopeChangeRequests/${scopeChangeRequest.id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send({
+          status: SCOPE_CHANGE_REQ_STATUS.APPROVED,
+        })
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            models.ScopeChangeRequest.findOne({ where: { id: scopeChangeRequest.id } }).then((_scopeChangeRequest) => {
+              assertStatus(_scopeChangeRequest, SCOPE_CHANGE_REQ_STATUS.APPROVED);
+              done();
+            });
+          }
+        });
+    });
+
+    it('Should activate change request with manager login', (done) => {
+      // Updating project details before activation. This is used in a later test case
+      updateProjectDetails(project.id, { apiDefinition: { notes: 'Please include swagger docs' } }).then(() => {
+        request(server)
+          .patch(`/v5/projects/${project.id}/scopeChangeRequests/${scopeChangeRequest.id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.manager}`,
+          })
+          .send({
+            status: SCOPE_CHANGE_REQ_STATUS.ACTIVATED,
+          })
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              models.ScopeChangeRequest.findOne({ where: { id: scopeChangeRequest.id } })
+                .then((_scopeChangeRequest) => {
+                  assertStatus(_scopeChangeRequest, SCOPE_CHANGE_REQ_STATUS.ACTIVATED);
+                  done();
+                });
+            }
+          });
+      });
+    });
+
+    it('Should update details field of project on activation', (done) => {
+      models.Project.findOne({ where: { id: project.id } }).then((_project) => {
+        const numberScreens = _.get(_project, 'details.appDefinition.numberScreens');
+        sinon.assert.match(numberScreens, '5-8');
+        done();
+      });
+    });
+
+    it("Should preserve fields of details json that doesn't change the scope on activation", (done) => {
+      models.Project.findOne({ where: { id: project.id } }).then((_project) => {
+        const apiNotes = _.get(_project, 'details.apiDefinition.notes');
+        sinon.assert.match(apiNotes, 'Please include swagger docs');
+        done();
+      });
+    });
+
+    it('Should not allow updating oldScope', (done) => {
+      request(server)
+        .patch(`/v5/projects/${project.id}/scopeChangeRequests/${scopeChangeRequest.id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({
+          oldScope: {},
+        })
+        .expect(400)
+        .end(err => done(err));
+    });
+
+    it('Should not allow updating newScope', (done) => {
+      request(server)
+        .patch(`/v5/projects/${project.id}/scopeChangeRequests/${scopeChangeRequest.id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({
+          newScope: {},
+        })
+        .expect(400)
+        .end(err => done(err));
+    });
+  });
+});
diff --git a/src/routes/timelines/create.spec.js b/src/routes/timelines/create.spec.js
index 6879987..8e94687 100644
--- a/src/routes/timelines/create.spec.js
+++ b/src/routes/timelines/create.spec.js
@@ -505,6 +505,15 @@ describe('CREATE timeline', () => {
             should.exist(milestone.updatedAt);
             should.not.exist(milestone.deletedBy);
             should.not.exist(milestone.deletedAt);
+
+            // validate statusHistory
+            should.exist(milestone.statusHistory);
+            milestone.statusHistory.should.be.an('array');
+            milestone.statusHistory.length.should.be.eql(1);
+            milestone.statusHistory.forEach((statusHistory) => {
+              statusHistory.reference.should.be.eql('milestone');
+              statusHistory.referenceId.should.be.eql(milestone.id);
+            });
           });
 
           // eslint-disable-next-line no-unused-expressions
diff --git a/src/routes/timelines/delete.spec.js b/src/routes/timelines/delete.spec.js
index e9f6d1c..e082578 100644
--- a/src/routes/timelines/delete.spec.js
+++ b/src/routes/timelines/delete.spec.js
@@ -159,6 +159,7 @@ describe('DELETE timeline', () => {
                 // Create milestones
                 models.Milestone.bulkCreate([
                   {
+                    id: 1,
                     timelineId: 1,
                     name: 'milestone 1',
                     duration: 2,
@@ -181,6 +182,7 @@ describe('DELETE timeline', () => {
                     updatedBy: 2,
                   },
                   {
+                    id: 2,
                     timelineId: 1,
                     name: 'milestone 2',
                     duration: 2,
diff --git a/src/routes/timelines/get.js b/src/routes/timelines/get.js
index d55b5af..f5145ff 100644
--- a/src/routes/timelines/get.js
+++ b/src/routes/timelines/get.js
@@ -20,8 +20,22 @@ const schema = {
   params: {
     timelineId: Joi.number().integer().positive().required(),
   },
+  query: {
+    db: Joi.boolean().optional(),
+  },
 };
 
+// Load the milestones
+const loadMilestones = timeline =>
+  timeline.getMilestones()
+    .then((milestones) => {
+      const loadedTimeline = _.omit(timeline.toJSON(), ['deletedAt', 'deletedBy']);
+      loadedTimeline.milestones =
+        _.map(milestones, milestone => _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy']));
+
+      return Promise.resolve(loadedTimeline);
+    });
+
 module.exports = [
   validate(schema),
   // Validate and get projectId from the timelineId param, and set to request params for
@@ -29,27 +43,24 @@ module.exports = [
   validateTimeline.validateTimelineIdParam,
   permissions('timeline.view'),
   (req, res, next) => {
-    eClient.get({ index: ES_TIMELINE_INDEX,
+    // when user query with db, bypass the elasticsearch
+    // and get the data directly from database
+    if (req.query.db) {
+      req.log.debug('bypass ES, gets timeline directly from database');
+      return loadMilestones(req.timeline).then(timeline => res.json(timeline));
+    }
+    return eClient.get({ index: ES_TIMELINE_INDEX,
       type: ES_TIMELINE_TYPE,
       id: req.params.timelineId,
     })
     .then((doc) => {
       req.log.debug('timeline found in ES');
-      res.json(doc._source);  // eslint-disable-line no-underscore-dangle
+      return res.json(doc._source);  // eslint-disable-line no-underscore-dangle
     })
     .catch((err) => {
       if (err.status === 404) {
         req.log.debug('No timeline found in ES');
-        // Load the milestones
-        return req.timeline.getMilestones()
-          .then((milestones) => {
-            const timeline = _.omit(req.timeline.toJSON(), ['deletedAt', 'deletedBy']);
-            timeline.milestones =
-              _.map(milestones, milestone => _.omit(milestone.toJSON(), ['deletedAt', 'deletedBy']));
-
-            // Write to response
-            return res.json(timeline);
-          });
+        return loadMilestones(req.timeline).then(timeline => res.json(timeline));
       }
       return next(err);
     });
diff --git a/src/routes/timelines/get.spec.js b/src/routes/timelines/get.spec.js
index 7bec2f8..82ac3b5 100644
--- a/src/routes/timelines/get.spec.js
+++ b/src/routes/timelines/get.spec.js
@@ -3,6 +3,8 @@
  */
 import chai from 'chai';
 import request from 'supertest';
+import config from 'config';
+import _ from 'lodash';
 
 import models from '../../models';
 import server from '../../app';
@@ -10,6 +12,42 @@ import testUtil from '../../tests/util';
 
 const should = chai.should();
 
+const ES_TIMELINE_INDEX = config.get('elasticsearchConfig.timelineIndexName');
+const ES_TIMELINE_TYPE = config.get('elasticsearchConfig.timelineDocType');
+
+const timelines = [
+  {
+    name: 'name 1',
+    description: 'description 1',
+    startDate: '2018-05-11T00:00:00.000Z',
+    endDate: '2018-05-12T00:00:00.000Z',
+    reference: 'project',
+    referenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    name: 'name 2',
+    description: 'description 2',
+    startDate: '2018-05-12T00:00:00.000Z',
+    endDate: '2018-05-13T00:00:00.000Z',
+    reference: 'phase',
+    referenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+  },
+  {
+    name: 'name 3',
+    description: 'description 3',
+    startDate: '2018-05-13T00:00:00.000Z',
+    endDate: '2018-05-14T00:00:00.000Z',
+    reference: 'phase',
+    referenceId: 1,
+    createdBy: 1,
+    updatedBy: 1,
+    deletedAt: '2018-05-14T00:00:00.000Z',
+  },
+];
 const milestones = [
   {
     id: 1,
@@ -143,41 +181,37 @@ describe('GET timeline', () => {
               ]))
               .then(() =>
                 // Create timelines
-                models.Timeline.bulkCreate([
-                  {
-                    name: 'name 1',
-                    description: 'description 1',
-                    startDate: '2018-05-11T00:00:00.000Z',
-                    endDate: '2018-05-12T00:00:00.000Z',
-                    reference: 'project',
-                    referenceId: 1,
-                    createdBy: 1,
-                    updatedBy: 1,
-                  },
-                  {
-                    name: 'name 2',
-                    description: 'description 2',
-                    startDate: '2018-05-12T00:00:00.000Z',
-                    endDate: '2018-05-13T00:00:00.000Z',
-                    reference: 'phase',
-                    referenceId: 1,
-                    createdBy: 1,
-                    updatedBy: 1,
-                  },
-                  {
-                    name: 'name 3',
-                    description: 'description 3',
-                    startDate: '2018-05-13T00:00:00.000Z',
-                    endDate: '2018-05-14T00:00:00.000Z',
-                    reference: 'phase',
-                    referenceId: 1,
-                    createdBy: 1,
-                    updatedBy: 1,
-                    deletedAt: '2018-05-14T00:00:00.000Z',
-                  },
-                ]))
-              .then(() => models.Milestone.bulkCreate(milestones))
-              .then(() => done());
+                // Create timelines
+                models.Timeline.bulkCreate(timelines, { returning: true })
+                  .then(createdTimelines => (
+                    // create milestones after timelines
+                    models.Milestone.bulkCreate(milestones))
+                      .then(createdMilestones => [createdTimelines, createdMilestones]),
+                  ),
+              ).then(([createdTimelines, createdMilestones]) =>
+                // Index to ES
+                Promise.all(_.map(createdTimelines, async (createdTimeline) => {
+                  const timelineJson = _.omit(createdTimeline.toJSON(), 'deletedAt', 'deletedBy');
+                  timelineJson.projectId = createdTimeline.id !== 3 ? 1 : 2;
+                  if (timelineJson.id === 1) {
+                    timelineJson.milestones = _.map(
+                      createdMilestones,
+                      cm => _.omit(cm.toJSON(), 'deletedAt', 'deletedBy'),
+                    );
+                  } else if (timelineJson.id === 2) {
+                    timelineJson.description = 'from ES';
+                  }
+
+                  await server.services.es.index({
+                    index: ES_TIMELINE_INDEX,
+                    type: ES_TIMELINE_TYPE,
+                    id: timelineJson.id,
+                    body: timelineJson,
+                  });
+                }))
+                  .then(() => {
+                    done();
+                  }));
           });
       });
   });
@@ -264,6 +298,16 @@ describe('GET timeline', () => {
 
           // Milestones
           resJson.milestones.should.have.length(2);
+          resJson.milestones.forEach((milestone) => {
+            // validate statusHistory
+            should.exist(milestone.statusHistory);
+            milestone.statusHistory.should.be.an('array');
+            milestone.statusHistory.length.should.be.eql(1);
+            milestone.statusHistory.forEach((statusHistory) => {
+              statusHistory.reference.should.be.eql('milestone');
+              statusHistory.referenceId.should.be.eql(milestone.id);
+            });
+          });
 
           done();
         });
@@ -306,5 +350,53 @@ describe('GET timeline', () => {
         })
         .expect(200, done);
     });
+
+    it('should return data from ES when db param is not set', (done) => {
+      request(server)
+        .get('/v5/timelines/2')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(2);
+          resJson.name.should.be.eql('name 2');
+          resJson.description.should.be.eql('from ES');
+
+          resJson.startDate.should.be.eql('2018-05-12T00:00:00.000Z');
+          resJson.endDate.should.be.eql('2018-05-13T00:00:00.000Z');
+          resJson.reference.should.be.eql('phase');
+          resJson.referenceId.should.be.eql(1);
+
+          resJson.createdBy.should.be.eql(1);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(1);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          should.not.exist(resJson.milestones);
+
+          done();
+        });
+    });
+
+    it('should return data from DB without calling ES when db param is set', (done) => {
+      request(server)
+        .get('/v5/timelines/2?db=true')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(2);
+          resJson.name.should.be.eql('name 2');
+          resJson.description.should.be.eql('description 2');
+
+          done();
+        });
+    });
   });
 });
diff --git a/src/routes/timelines/list.spec.js b/src/routes/timelines/list.spec.js
index 5ebe767..3a3e999 100644
--- a/src/routes/timelines/list.spec.js
+++ b/src/routes/timelines/list.spec.js
@@ -3,7 +3,7 @@
  */
 import chai from 'chai';
 import request from 'supertest';
-import sleep from 'sleep';
+// import sleep from 'sleep';
 import config from 'config';
 import _ from 'lodash';
 
@@ -182,25 +182,34 @@ describe('LIST timelines', () => {
                ]))
               .then(() =>
                 // Create timelines
-                 models.Timeline.bulkCreate(timelines, { returning: true }))
-              .then(createdTimelines =>
+                models.Timeline.bulkCreate(timelines, { returning: true })
+                  .then(createdTimelines => (
+                    // create milestones after timelines
+                    models.Milestone.bulkCreate(milestones))
+                      .then(createdMilestones => [createdTimelines, createdMilestones]),
+                  ),
+              ).then(([createdTimelines, createdMilestones]) =>
                 // Index to ES
-                 Promise.all(_.map(createdTimelines, (createdTimeline) => {
-                   const timelineJson = _.omit(createdTimeline.toJSON(), 'deletedAt', 'deletedBy');
-                   timelineJson.projectId = createdTimeline.id !== 3 ? 1 : 2;
-                   if (timelineJson.id === 1) {
-                     timelineJson.milestones = milestones;
-                   }
-                   return server.services.es.index({
-                     index: ES_TIMELINE_INDEX,
-                     type: ES_TIMELINE_TYPE,
-                     id: timelineJson.id,
-                     body: timelineJson,
-                   });
-                 }))
+                Promise.all(_.map(createdTimelines, (createdTimeline) => {
+                  const timelineJson = _.omit(createdTimeline.toJSON(), 'deletedAt', 'deletedBy');
+                  timelineJson.projectId = createdTimeline.id !== 3 ? 1 : 2;
+                  if (timelineJson.id === 1) {
+                    timelineJson.milestones = _.map(
+                      createdMilestones,
+                      cm => _.omit(cm.toJSON(), 'deletedAt', 'deletedBy'),
+                    );
+                  }
+
+                  return server.services.es.index({
+                    index: ES_TIMELINE_INDEX,
+                    type: ES_TIMELINE_TYPE,
+                    id: timelineJson.id,
+                    body: timelineJson,
+                  });
+                }))
                   .then(() => {
                     // sleep for some time, let elasticsearch indices be settled
-                    sleep.sleep(5);
+                    // sleep.sleep(5);
                     done();
                   }));
           });
@@ -278,6 +287,16 @@ describe('LIST timelines', () => {
 
           // Milestones
           resJson[0].milestones.should.have.length(2);
+          resJson[0].milestones.forEach((milestone) => {
+            // validate statusHistory
+            should.exist(milestone.statusHistory);
+            milestone.statusHistory.should.be.an('array');
+            milestone.statusHistory.length.should.be.eql(1);
+            milestone.statusHistory.forEach((statusHistory) => {
+              statusHistory.reference.should.be.eql('milestone');
+              statusHistory.referenceId.should.be.eql(milestone.id);
+            });
+          });
 
           done();
         });
diff --git a/src/routes/timelines/update.js b/src/routes/timelines/update.js
index e4c8fd5..885d7db 100644
--- a/src/routes/timelines/update.js
+++ b/src/routes/timelines/update.js
@@ -107,7 +107,8 @@ module.exports = [
           req,
           EVENT.ROUTING_KEY.TIMELINE_UPDATED,
           RESOURCES.TIMELINE,
-          _.assign(entityToUpdate, _.pick(updated, 'id', 'updatedAt')));
+          updated,
+          original);
 
         // Write to response
         res.json(updated);
diff --git a/src/routes/timelines/update.spec.js b/src/routes/timelines/update.spec.js
index 1d08ccc..73ae626 100644
--- a/src/routes/timelines/update.spec.js
+++ b/src/routes/timelines/update.spec.js
@@ -9,7 +9,7 @@ import _ from 'lodash';
 import models from '../../models';
 import server from '../../app';
 import testUtil from '../../tests/util';
-import { EVENT, BUS_API_EVENT, RESOURCES } from '../../constants';
+import { EVENT, BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
 import busApi from '../../services/busApi';
 
 const should = chai.should();
@@ -470,6 +470,19 @@ describe('UPDATE timeline', () => {
           should.not.exist(resJson.deletedAt);
           should.not.exist(resJson.deletedBy);
 
+          // Milestones
+          resJson.milestones.should.have.length(2);
+          resJson.milestones.forEach((milestone) => {
+            // validate statusHistory
+            should.exist(milestone.statusHistory);
+            milestone.statusHistory.should.be.an('array');
+            milestone.statusHistory.length.should.be.eql(1);
+            milestone.statusHistory.forEach((statusHistory) => {
+              statusHistory.reference.should.be.eql('milestone');
+              statusHistory.referenceId.should.be.eql(milestone.id);
+            });
+          });
+
           // eslint-disable-next-line no-unused-expressions
           server.services.pubsub.publish.calledWith(EVENT.ROUTING_KEY.TIMELINE_UPDATED).should.be.true;
 
@@ -621,7 +634,7 @@ describe('UPDATE timeline', () => {
 
       // not testing fields separately as startDate is required parameter,
       // thus TIMELINE_ADJUSTED will be always sent
-      it('should send message BUS_API_EVENT.TIMELINE_UPDATED when timeline updated', (done) => {
+      it('should send correct BUS API messages when timeline updated', (done) => {
         request(server)
           .patch('/v5/timelines/1')
           .set({
@@ -634,9 +647,22 @@ describe('UPDATE timeline', () => {
               done(err);
             } else {
               testUtil.wait(() => {
-                createEventSpy.calledOnce.should.be.true;
-                createEventSpy.calledWith(BUS_API_EVENT.TIMELINE_UPDATED,
-                  sinon.match({ resource: RESOURCES.TIMELINE })).should.be.true;
+                createEventSpy.callCount.should.equal(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.TIMELINE_UPDATED, sinon.match({
+                  resource: RESOURCES.TIMELINE,
+                  name: body.name,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.TIMELINE_ADJUSTED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051332,
+                  initiatorUserId: 40051332,
+                })).should.be.true;
+
                 done();
               });
             }
diff --git a/src/routes/workItems/create.js b/src/routes/workItems/create.js
new file mode 100644
index 0000000..f53ef1b
--- /dev/null
+++ b/src/routes/workItems/create.js
@@ -0,0 +1,139 @@
+/**
+ * API to add a work item
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES } from '../../constants';
+
+const permissions = require('tc-core-library-js').middleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    workId: Joi.number().integer().positive().required(),
+  },
+  body: {
+    name: Joi.string().required(),
+    type: Joi.string().required(),
+    templateId: Joi.number().positive().optional(),
+    directProjectId: Joi.number().positive().optional(),
+    billingAccountId: Joi.number().positive().optional(),
+    estimatedPrice: Joi.number().positive().optional(),
+    actualPrice: Joi.number().positive().optional(),
+    details: Joi.any().optional(),
+  },
+};
+
+module.exports = [
+  // validate request payload
+  validate(schema),
+  // check permission
+  permissions('workItem.create'),
+  // do the real work
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.workId);
+
+    const data = req.body;
+    // default values
+    _.assign(data, {
+      projectId,
+      phaseId,
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+    });
+
+    let newPhaseProduct = null;
+    models.sequelize.transaction(() => models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+        projectId,
+      },
+      include: [{
+        model: models.WorkStream,
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      }],
+    }).then((existing) => {
+      // make sure work stream exists
+      if (!existing) {
+        const err = new Error(`project work stream not found for project id ${projectId}` +
+          ` and work stream ${workStreamId} and phase id ${phaseId}`);
+        err.status = 404;
+        throw err;
+      }
+
+      return models.Project.findOne({
+        where: { id: projectId, deletedAt: { $eq: null } },
+        raw: true,
+      });
+    })
+    .then((existingProject) => {
+      // make sure project exists
+      if (!existingProject) {
+        const err = new Error(`project not found for project id ${projectId}`);
+        err.status = 404;
+        throw err;
+      }
+
+      _.assign(data, {
+        phaseId,
+        projectId,
+        directProjectId: existingProject.directProjectId,
+        billingAccountId: existingProject.billingAccountId,
+      });
+
+      return models.PhaseProduct.count({
+        where: {
+          projectId,
+          phaseId,
+          deletedAt: { $eq: null },
+        },
+        raw: true,
+      });
+    })
+    .then((productCount) => {
+      // make sure number of products of per phase <= max value
+      if (productCount >= 100) {
+        const err = new Error('the number of products per phase cannot exceed ' +
+          `${100}`);
+        err.status = 400;
+        throw err;
+      }
+      return models.PhaseProduct.create(data)
+      .then((_newPhaseProduct) => {
+        newPhaseProduct = _.cloneDeep(_newPhaseProduct);
+        req.log.debug('new work created (id# %d, name: %s)',
+          newPhaseProduct.id, newPhaseProduct.name);
+        newPhaseProduct = newPhaseProduct.get({ plain: true });
+        newPhaseProduct = _.omit(newPhaseProduct, ['deletedAt', 'utm']);
+      });
+    }))
+    .then(() => {
+      // Send events to buses
+      req.log.debug('Sending event to RabbitMQ bus for phase product %d', newPhaseProduct.id);
+      req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED,
+        newPhaseProduct,
+        { correlationId: req.id },
+      );
+      req.log.debug('Sending event to Kafka bus for phase product %d', newPhaseProduct.id);
+      // emit the event
+      util.sendResourceToKafkaBus(
+        req,
+        EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_ADDED,
+        RESOURCES.PHASE_PRODUCT,
+        newPhaseProduct);
+
+      res.status(201).json(newPhaseProduct);
+    })
+    .catch((err) => { next(err); });
+  },
+];
diff --git a/src/routes/workItems/create.spec.js b/src/routes/workItems/create.spec.js
new file mode 100644
index 0000000..0e7e348
--- /dev/null
+++ b/src/routes/workItems/create.spec.js
@@ -0,0 +1,344 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for create.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+import sinon from 'sinon';
+
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+
+import { BUS_API_EVENT, RESOURCES } from '../../constants';
+
+const should = chai.should();
+
+const body = {
+  name: 'test phase product',
+  type: 'product1',
+  estimatedPrice: 20.0,
+  actualPrice: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+};
+
+describe('CREATE Work Item', () => {
+  let projectId;
+  let workStreamId;
+  let workId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'workItem.create',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                models.ProjectPhase.create({
+                  name: 'test project phase',
+                  status: 'active',
+                  startDate: '2018-05-15T00:00:00Z',
+                  endDate: '2018-05-15T12:00:00Z',
+                  budget: 20.0,
+                  progress: 1.23456,
+                  details: {
+                    message: 'This can be any json',
+                  },
+                  createdBy: 1,
+                  updatedBy: 1,
+                  projectId,
+                }).then((phase) => {
+                  workId = phase.id;
+                  models.PhaseWorkStream.create({
+                    phaseId: workId,
+                    workStreamId,
+                  }).then(() => {
+                    // create members
+                    models.ProjectMember.bulkCreate([{
+                      id: 1,
+                      userId: copilotUser.userId,
+                      projectId,
+                      role: 'copilot',
+                      isPrimary: false,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }, {
+                      id: 2,
+                      userId: memberUser.userId,
+                      projectId,
+                      role: 'customer',
+                      isPrimary: true,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }]).then(() => done());
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+  });
+
+  afterEach((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('POST /projects/{projectId}/workstreams/{workStreamId}/works/{workId}/workitems', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/999/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 400 when name not provided', (done) => {
+      const reqBody = _.cloneDeep(body);
+      delete reqBody.name;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({ param: reqBody })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when type not provided', (done) => {
+      const reqBody = _.cloneDeep(body);
+      delete reqBody.type;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({ param: reqBody })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when estimatedPrice is negative', (done) => {
+      const reqBody = _.cloneDeep(body);
+      reqBody.estimatedPrice = -20;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({ param: reqBody })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when actualPrice is negative', (done) => {
+      const reqBody = _.cloneDeep(body);
+      reqBody.actualPrice = -20;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({ param: reqBody })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 200 for member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.name.should.be.eql(body.name);
+            resJson.type.should.be.eql(body.type);
+            resJson.estimatedPrice.should.be.eql(body.estimatedPrice);
+            resJson.actualPrice.should.be.eql(body.actualPrice);
+            resJson.details.should.be.eql(body.details);
+            done();
+          }
+        });
+    });
+
+    it('should return 201 if payload is valid', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.name.should.be.eql(body.name);
+            resJson.type.should.be.eql(body.type);
+            resJson.estimatedPrice.should.be.eql(body.estimatedPrice);
+            resJson.actualPrice.should.be.eql(body.actualPrice);
+            resJson.details.should.be.eql(body.details);
+            done();
+          }
+        });
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when work item created', (done) => {
+        request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED, sinon.match({
+                resource: RESOURCES.PHASE_PRODUCT,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+  });
+});
diff --git a/src/routes/workItems/delete.js b/src/routes/workItems/delete.js
new file mode 100644
index 0000000..bab0380
--- /dev/null
+++ b/src/routes/workItems/delete.js
@@ -0,0 +1,98 @@
+/**
+ * API to delete a work item
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    workId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  // check permission
+  permissions('workItem.delete'),
+
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.workId);
+    const productId = _.parseInt(req.params.id);
+
+    models.sequelize.transaction(() =>
+    models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+      },
+      include: [{
+        model: models.WorkStream,
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      },
+      ],
+    })
+    .then((existing) => {
+      if (!existing) {
+        // handle 404
+        const err = new Error('No active work item found for project id ' +
+          `${projectId}, phase id ${phaseId} and work stream id ${workStreamId}`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+
+      // soft delete the record
+      return models.PhaseProduct.findOne({
+        where: {
+          id: productId,
+          projectId,
+          phaseId,
+          deletedAt: { $eq: null },
+        },
+      });
+    })
+    .then((existing) => {
+      if (!existing) {
+          // handle 404
+        const err = new Error('No active work item found for project id ' +
+            `${projectId}, phase id ${phaseId} and product id ${productId}`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+      return existing.update({ deletedBy: req.authUser.userId });
+    })
+      .then(entity => entity.destroy()))
+      .then((deleted) => {
+        req.log.debug('deleted work item', JSON.stringify(deleted, null, 2));
+
+        // Send events to buses
+        req.app.services.pubsub.publish(
+          EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED,
+          deleted,
+          { correlationId: req.id },
+        );
+        // emit the event
+        util.sendResourceToKafkaBus(
+          req,
+          EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_REMOVED,
+          RESOURCES.PHASE_PRODUCT,
+          _.pick(deleted.toJSON(), 'id'));
+
+        res.status(204).json({});
+      })
+      .catch(err => next(err));
+  },
+];
diff --git a/src/routes/workItems/delete.spec.js b/src/routes/workItems/delete.spec.js
new file mode 100644
index 0000000..f9e9873
--- /dev/null
+++ b/src/routes/workItems/delete.spec.js
@@ -0,0 +1,291 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for delete.js
+ */
+import _ from 'lodash';
+import request from 'supertest';
+import chai from 'chai';
+import sinon from 'sinon';
+
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+
+import { BUS_API_EVENT, RESOURCES } from '../../constants';
+
+chai.should();
+
+const expectAfterDelete = (projectId, workStreamId, phaseId, id, err, next) => {
+  if (err) throw err;
+  setTimeout(() =>
+  models.PhaseProduct.findOne({
+    where: {
+      id,
+      projectId,
+      phaseId,
+    },
+    paranoid: false,
+  })
+    .then((res) => {
+      if (!res) {
+        throw new Error('Should found the entity');
+      } else {
+        chai.assert.isNotNull(res.deletedAt);
+        chai.assert.isNotNull(res.deletedBy);
+
+        request(server)
+          .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${phaseId}/workitems/${id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(404, next);
+      }
+    }), 500);
+};
+const body = {
+  name: 'test phase product',
+  type: 'product1',
+  estimatedPrice: 20.0,
+  actualPrice: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+  createdBy: 1,
+  updatedBy: 1,
+};
+
+describe('DELETE Work Item', () => {
+  let projectId;
+  let workStreamId;
+  let workId;
+  let productId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'workItem.delete',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                models.ProjectPhase.create({
+                  name: 'test project phase',
+                  status: 'active',
+                  startDate: '2018-05-15T00:00:00Z',
+                  endDate: '2018-05-15T12:00:00Z',
+                  budget: 20.0,
+                  progress: 1.23456,
+                  details: {
+                    message: 'This can be any json',
+                  },
+                  createdBy: 1,
+                  updatedBy: 1,
+                  projectId,
+                }).then((phase) => {
+                  workId = phase.id;
+                  models.PhaseWorkStream.create({
+                    phaseId: workId,
+                    workStreamId,
+                  })
+                  .then(() => {
+                    _.assign(body, { phaseId: workId, projectId });
+                    models.PhaseProduct.create(body).then((product) => {
+                      productId = product.id;
+                      // create members
+                      models.ProjectMember.bulkCreate([{
+                        id: 1,
+                        userId: copilotUser.userId,
+                        projectId,
+                        role: 'copilot',
+                        isPrimary: false,
+                        createdBy: 1,
+                        updatedBy: 1,
+                      }, {
+                        id: 2,
+                        userId: memberUser.userId,
+                        projectId,
+                        role: 'customer',
+                        isPrimary: true,
+                        createdBy: 1,
+                        updatedBy: 1,
+                      }]).then(() => done());
+                    });
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+  });
+
+  afterEach((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('DELETE /projects/{projectId}/workstreams/{workStreamId}/works/{workId}/workitems/{productId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/999/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 204 for member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(204, done);
+    });
+
+    it('should return 204 when user have project permission', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(projectId, workStreamId, workId, productId, err, done));
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when work item removed', (done) => {
+        request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(204)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED, sinon.match({
+                resource: RESOURCES.PHASE_PRODUCT,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+  });
+});
diff --git a/src/routes/workItems/get.js b/src/routes/workItems/get.js
new file mode 100644
index 0000000..44d2071
--- /dev/null
+++ b/src/routes/workItems/get.js
@@ -0,0 +1,74 @@
+/**
+ * API to get a work item
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import models from '../../models';
+
+const permissions = require('tc-core-library-js').middleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    workId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  // check permission
+  permissions('workItem.view'),
+
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.workId);
+    const productId = _.parseInt(req.params.id);
+
+    models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+        projectId,
+      },
+      include: [{
+        model: models.WorkStream,
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      },
+      ],
+    })
+    .then((existing) => {
+      if (!existing) {
+        // handle 404
+        const err = new Error('No active work item found for project id ' +
+          `${projectId}, phase id ${phaseId} and work stream id ${workStreamId}`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+
+      return models.PhaseProduct.findOne({
+        where: {
+          id: productId,
+          projectId,
+          phaseId,
+          deletedAt: { $eq: null },
+        },
+      });
+    }).then((product) => {
+      if (!product) {
+        // handle 404
+        const err = new Error('phase product not found for project id ' +
+              `${projectId}, phase id ${phaseId} and product id ${productId}`);
+        err.status = 404;
+        throw err;
+      } else {
+        res.json(product);
+      }
+    }).catch(err => next(err));
+  },
+];
diff --git a/src/routes/workItems/get.spec.js b/src/routes/workItems/get.spec.js
new file mode 100644
index 0000000..b3acd08
--- /dev/null
+++ b/src/routes/workItems/get.spec.js
@@ -0,0 +1,234 @@
+/**
+ * Tests for get.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+const body = {
+  name: 'test phase product',
+  type: 'product1',
+  estimatedPrice: 20.0,
+  actualPrice: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+  createdBy: 1,
+  updatedBy: 1,
+};
+
+describe('GET Work Item', () => {
+  let projectId;
+  let workStreamId;
+  let workId;
+  let productId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+          .then((template) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              // create members
+              models.ProjectMember.bulkCreate([{
+                id: 1,
+                userId: copilotUser.userId,
+                projectId,
+                role: 'copilot',
+                isPrimary: false,
+                createdBy: 1,
+                updatedBy: 1,
+              }, {
+                id: 2,
+                userId: memberUser.userId,
+                projectId,
+                role: 'customer',
+                isPrimary: true,
+                createdBy: 1,
+                updatedBy: 1,
+              }])
+              .then(() => {
+                models.WorkStream.create({
+                  name: 'Work Stream',
+                  type: 'generic',
+                  status: 'active',
+                  projectId,
+                  createdBy: 1,
+                  updatedBy: 1,
+                }).then((entity) => {
+                  workStreamId = entity.id;
+                  models.ProjectPhase.create({
+                    name: 'test project phase',
+                    status: 'active',
+                    startDate: '2018-05-15T00:00:00Z',
+                    endDate: '2018-05-15T12:00:00Z',
+                    budget: 20.0,
+                    progress: 1.23456,
+                    details: {
+                      message: 'This can be any json',
+                    },
+                    createdBy: 1,
+                    updatedBy: 1,
+                    projectId,
+                  }).then((phase) => {
+                    workId = phase.id;
+                    models.PhaseWorkStream.create({
+                      phaseId: workId,
+                      workStreamId,
+                    })
+                    .then(() => {
+                      _.assign(body, { phaseId: workId, projectId });
+                      models.PhaseProduct.create(body).then((product) => {
+                        productId = product.id;
+                        done();
+                      });
+                    });
+                  });
+                });
+              });
+            });
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams/{workStreamId}/works/{workId}/workitems/{productId}', () => {
+    it('should return 403 when user have no permission (non team member)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(403, done);
+    });
+
+    it('should return 404 when no project with specific projectId', (done) => {
+      request(server)
+        .get(`/v5/projects/9999/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/999/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 1 phase when user have project permission (customer)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.name.should.be.eql(body.name);
+            resJson.type.should.be.eql(body.type);
+            resJson.estimatedPrice.should.be.eql(body.estimatedPrice);
+            resJson.actualPrice.should.be.eql(body.actualPrice);
+            resJson.details.should.be.eql(body.details);
+            done();
+          }
+        });
+    });
+
+    it('should return 1 phase when user have project permission (copilot)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.name.should.be.eql(body.name);
+            resJson.type.should.be.eql(body.type);
+            resJson.estimatedPrice.should.be.eql(body.estimatedPrice);
+            resJson.actualPrice.should.be.eql(body.actualPrice);
+            resJson.details.should.be.eql(body.details);
+            done();
+          }
+        });
+    });
+  });
+});
diff --git a/src/routes/workItems/list.js b/src/routes/workItems/list.js
new file mode 100644
index 0000000..b9dd656
--- /dev/null
+++ b/src/routes/workItems/list.js
@@ -0,0 +1,63 @@
+/**
+ * API to get a list of work items
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import models from '../../models';
+
+const permissions = require('tc-core-library-js').middleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    workId: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  // validate request payload
+  validate(schema),
+  // check permission
+  permissions('workItem.view'),
+
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.workId);
+
+    models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+        projectId,
+      },
+      include: [{
+        model: models.WorkStream,
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      },
+      ],
+    })
+    .then((existing) => {
+      if (!existing) {
+          // handle 404
+        const err = new Error('No active phase product found for project id ' +
+              `${projectId}, work stream id ${workStreamId} and phase id ${phaseId}`);
+        err.status = 404;
+        throw err;
+      }
+
+      return models.PhaseProduct.findAll({
+        where: {
+          phaseId,
+          projectId,
+        },
+      });
+    })
+    .then(products => res.json(products))
+    .catch(err => next(err));
+  },
+];
diff --git a/src/routes/workItems/list.spec.js b/src/routes/workItems/list.spec.js
new file mode 100644
index 0000000..bb2cd9f
--- /dev/null
+++ b/src/routes/workItems/list.spec.js
@@ -0,0 +1,225 @@
+/**
+ * Tests for list.js
+ */
+import _ from 'lodash';
+import request from 'supertest';
+import chai from 'chai';
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+const body = {
+  name: 'test phase product',
+  type: 'product1',
+  estimatedPrice: 20.0,
+  actualPrice: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+  createdBy: 1,
+  updatedBy: 1,
+};
+
+describe('LIST Work Items', () => {
+  let projectId;
+  let workStreamId;
+  let workId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+          .then((template) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              // create members
+              models.ProjectMember.bulkCreate([{
+                id: 1,
+                userId: copilotUser.userId,
+                projectId,
+                role: 'copilot',
+                isPrimary: false,
+                createdBy: 1,
+                updatedBy: 1,
+              }, {
+                id: 2,
+                userId: memberUser.userId,
+                projectId,
+                role: 'customer',
+                isPrimary: true,
+                createdBy: 1,
+                updatedBy: 1,
+              }])
+              .then(() => {
+                models.WorkStream.create({
+                  name: 'Work Stream',
+                  type: 'generic',
+                  status: 'active',
+                  projectId,
+                  createdBy: 1,
+                  updatedBy: 1,
+                }).then((entity) => {
+                  workStreamId = entity.id;
+                  models.ProjectPhase.create({
+                    name: 'test project phase',
+                    status: 'active',
+                    startDate: '2018-05-15T00:00:00Z',
+                    endDate: '2018-05-15T12:00:00Z',
+                    budget: 20.0,
+                    progress: 1.23456,
+                    details: {
+                      message: 'This can be any json',
+                    },
+                    createdBy: 1,
+                    updatedBy: 1,
+                    projectId,
+                  }).then((phase) => {
+                    workId = phase.id;
+                    models.PhaseWorkStream.create({
+                      phaseId: workId,
+                      workStreamId,
+                    })
+                    .then(() => {
+                      _.assign(body, { phaseId: workId, projectId });
+                      models.PhaseProduct.create(body).then(() => done());
+                    });
+                  });
+                });
+              });
+            });
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams/{workStreamId}/works/{workId}/workitems', () => {
+    it('should return 403 when user have no permission (non team member)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(403, done);
+    });
+
+    it('should return 404 when no project with specific projectId', (done) => {
+      request(server)
+        .get(`/v5/projects/9999/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/999/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 1 phase when user have project permission (customer)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(1);
+            done();
+          }
+        });
+    });
+
+    it('should return 1 phase when user have project permission (copilot)', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.should.have.lengthOf(1);
+            done();
+          }
+        });
+    });
+  });
+});
diff --git a/src/routes/workItems/update.js b/src/routes/workItems/update.js
new file mode 100644
index 0000000..d22e00b
--- /dev/null
+++ b/src/routes/workItems/update.js
@@ -0,0 +1,119 @@
+/**
+ * API to update a work item
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES, ROUTES } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    workId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+  body: {
+    name: Joi.string().optional(),
+    type: Joi.string().optional(),
+    templateId: Joi.number().positive().optional(),
+    directProjectId: Joi.number().positive().optional(),
+    billingAccountId: Joi.number().positive().optional(),
+    estimatedPrice: Joi.number().positive().optional(),
+    actualPrice: Joi.number().positive().optional(),
+    details: Joi.any().optional(),
+  },
+};
+
+
+module.exports = [
+  // validate request payload
+  validate(schema),
+  // check permission
+  permissions('workItem.edit'),
+
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.workId);
+    const productId = _.parseInt(req.params.id);
+
+    const updatedProps = req.body;
+    updatedProps.updatedBy = req.authUser.userId;
+
+    let previousValue;
+
+    models.sequelize.transaction(() => models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+        projectId,
+      },
+      include: [{
+        model: models.WorkStream,
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      },
+      ],
+    })
+    .then((existingWork) => {
+      if (!existingWork) {
+        // handle 404
+        const err = new Error('No active work item found for project id ' +
+          `${projectId}, phase id ${phaseId} and work stream id ${workStreamId}`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+
+      return models.PhaseProduct.findOne({
+        where: {
+          id: productId,
+          projectId,
+          phaseId,
+          deletedAt: { $eq: null },
+        },
+      });
+    })
+    .then((existing) => {
+      if (!existing) {
+          // handle 404
+        const err = new Error('No active phase product found for project id ' +
+              `${projectId}, phase id ${phaseId} and product id ${productId}`);
+        err.status = 404;
+        throw err;
+      }
+
+      previousValue = _.clone(existing.get({ plain: true }));
+      _.extend(existing, updatedProps);
+      return existing.save().catch(next);
+    }))
+    .then((updated) => {
+      req.log.debug('updated work item', JSON.stringify(updated, null, 2));
+
+      const updatedValue = updated.get({ plain: true });
+
+      // emit original and updated project phase information
+      req.app.services.pubsub.publish(
+        EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED,
+        { original: previousValue, updated: updatedValue },
+        { correlationId: req.id },
+      );
+      util.sendResourceToKafkaBus(
+        req,
+        EVENT.ROUTING_KEY.PROJECT_PHASE_PRODUCT_UPDATED,
+        RESOURCES.PHASE_PRODUCT,
+        updatedValue,
+        previousValue,
+        ROUTES.WORK_ITEMS.UPDATE,
+      );
+
+      res.json(updated);
+    }).catch(err => next(err));
+  },
+];
diff --git a/src/routes/workItems/update.spec.js b/src/routes/workItems/update.spec.js
new file mode 100644
index 0000000..311e716
--- /dev/null
+++ b/src/routes/workItems/update.spec.js
@@ -0,0 +1,466 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for update.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+import sinon from 'sinon';
+
+import server from '../../app';
+import models from '../../models';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
+
+const should = chai.should();
+
+const body = {
+  name: 'test phase product',
+  type: 'product1',
+  estimatedPrice: 20.0,
+  actualPrice: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+  createdBy: 1,
+  updatedBy: 1,
+};
+
+const updateBody = {
+  name: 'test phase product xxx',
+  type: 'product2',
+  estimatedPrice: 123456.789,
+  actualPrice: 9.8765432,
+  details: {
+    message: 'This is another json',
+  },
+};
+
+describe('UPDATE Work Item', () => {
+  let projectId;
+  let workStreamId;
+  let workId;
+  let productId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'workItem.edit',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                models.ProjectPhase.create({
+                  name: 'test project phase',
+                  status: 'active',
+                  startDate: '2018-05-15T00:00:00Z',
+                  endDate: '2018-05-15T12:00:00Z',
+                  budget: 20.0,
+                  progress: 1.23456,
+                  details: {
+                    message: 'This can be any json',
+                  },
+                  createdBy: 1,
+                  updatedBy: 1,
+                  projectId,
+                }).then((phase) => {
+                  workId = phase.id;
+                  models.PhaseWorkStream.create({
+                    phaseId: workId,
+                    workStreamId,
+                  })
+                  .then(() => {
+                    _.assign(body, { phaseId: workId, projectId });
+                    models.PhaseProduct.create(body).then((product) => {
+                      productId = product.id;
+                      // create members
+                      models.ProjectMember.bulkCreate([{
+                        id: 1,
+                        userId: copilotUser.userId,
+                        projectId,
+                        role: 'copilot',
+                        isPrimary: false,
+                        createdBy: 1,
+                        updatedBy: 1,
+                      }, {
+                        id: 2,
+                        userId: memberUser.userId,
+                        projectId,
+                        role: 'customer',
+                        isPrimary: true,
+                        createdBy: 1,
+                        updatedBy: 1,
+                      }]).then(() => done());
+                    });
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH/projects/{projectId}/workstreams/{workStreamId}/works/{workId}/workitems/{productId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .send(updateBody)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(updateBody)
+        .expect(403, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/999/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 400 when parameters are invalid', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999/workitems/99999`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({
+          param: {
+            estimatedPrice: -15,
+          },
+        })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 200 for member', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(updateBody)
+        .expect(200, done);
+    });
+
+    it('should return updated product when user have permission and parameters are valid', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            should.exist(resJson);
+            resJson.name.should.be.eql(updateBody.name);
+            resJson.type.should.be.eql(updateBody.type);
+            resJson.estimatedPrice.should.be.eql(updateBody.estimatedPrice);
+            resJson.actualPrice.should.be.eql(updateBody.actualPrice);
+            resJson.details.should.be.eql(updateBody.details);
+            done();
+          }
+        });
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when name updated', (done) => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send({
+            name: 'new name',
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              testUtil.wait(() => {
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  name: 'new name',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051331,
+                  initiatorUserId: 40051331,
+                })).should.be.true;
+
+                done();
+              });
+            }
+          });
+      });
+
+      it('should send correct BUS API messages when estimatedPrice updated', (done) => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send({
+            estimatedPrice: 123,
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              testUtil.wait(() => {
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  estimatedPrice: 123,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051331,
+                  initiatorUserId: 40051331,
+                })).should.be.true;
+
+                done();
+              });
+            }
+          });
+      });
+
+      it('should send correct BUS API messages when actualPrice updated', (done) => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send({
+            actualPrice: 123,
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              testUtil.wait(() => {
+                createEventSpy.callCount.should.be.eql(2);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  actualPrice: 123,
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051331,
+                  initiatorUserId: 40051331,
+                })).should.be.true;
+
+                done();
+              });
+            }
+          });
+      });
+
+      it('should send correct BUS API messages when details updated', (done) => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send({
+            details: 'something',
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              testUtil.wait(() => {
+                createEventSpy.callCount.should.be.eql(3);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  details: 'something',
+                })).should.be.true;
+
+                // Check Notification Service events
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORKITEM_SPECIFICATION_MODIFIED)
+                  .should.be.true;
+                createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                  projectId: 1,
+                  projectName: 'test1',
+                  projectUrl: 'https://local.topcoder-dev.com/projects/1',
+                  userId: 40051331,
+                  initiatorUserId: 40051331,
+                })).should.be.true;
+
+                done();
+              });
+            }
+          });
+      });
+
+      it('should send correct BUS API messages when type updated', (done) => {
+        request(server)
+          .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}/workitems/${productId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.member}`,
+          })
+          .send({
+            type: 'another type',
+          })
+          .expect('Content-Type', /json/)
+          .expect(200)
+          .end((err) => {
+            if (err) {
+              done(err);
+            } else {
+              testUtil.wait(() => {
+                createEventSpy.callCount.should.be.eql(1);
+
+                createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_PRODUCT_UPDATED, sinon.match({
+                  resource: RESOURCES.PHASE_PRODUCT,
+                  type: 'another type',
+                })).should.be.true;
+
+                done();
+              });
+            }
+          });
+      });
+    });
+  });
+});
diff --git a/src/routes/workManagementPermissions/create.js b/src/routes/workManagementPermissions/create.js
new file mode 100644
index 0000000..d5be1c7
--- /dev/null
+++ b/src/routes/workManagementPermissions/create.js
@@ -0,0 +1,59 @@
+/* eslint-disable max-len */
+/**
+ * API to add a work management permission
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  body: Joi.object().keys({
+    policy: Joi.string().max(255).required(),
+    permission: Joi.object().required(),
+    projectTemplateId: Joi.number().integer().positive().required(),
+    createdAt: Joi.any().strip(),
+    updatedAt: Joi.any().strip(),
+    deletedAt: Joi.any().strip(),
+    createdBy: Joi.any().strip(),
+    updatedBy: Joi.any().strip(),
+    deletedBy: Joi.any().strip(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workManagementPermission.create'),
+  (req, res, next) => {
+    const entity = _.assign(req.body, {
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+    });
+
+    // Check if already exists
+    return models.WorkManagementPermission.findOne({
+      where: {
+        policy: entity.policy,
+        projectTemplateId: entity.projectTemplateId,
+      },
+      paranoid: false,
+    })
+      .then((existing) => {
+        if (existing) {
+          const apiErr = new Error(`Work Management Permission already exists (may be deleted) for policy "${entity.policy}" and project template id ${entity.projectTemplateId}`);
+          apiErr.status = 400;
+          return Promise.reject(apiErr);
+        }
+
+        // Create
+        return models.WorkManagementPermission.create(entity);
+      }).then((createdEntity) => {
+        // Omit deletedAt, deletedBy
+        res.status(201).json(_.omit(createdEntity.toJSON(), 'deletedAt', 'deletedBy'));
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/workManagementPermissions/create.spec.js b/src/routes/workManagementPermissions/create.spec.js
new file mode 100644
index 0000000..8c8eb19
--- /dev/null
+++ b/src/routes/workManagementPermissions/create.spec.js
@@ -0,0 +1,203 @@
+/**
+ * Tests for create.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import server from '../../app';
+import testUtil from '../../tests/util';
+import models from '../../models';
+
+const should = chai.should();
+
+describe('CREATE work management permission', () => {
+  let templateId;
+
+  const body = {
+    policy: 'work.create',
+    permission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+      },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((t) => {
+          templateId = t.id;
+          body.projectTemplateId = templateId;
+        }).then(() => done());
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('POST /projects/metadata/workManagementPermission', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-member', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 400 for missing policy', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.policy;
+
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing permission', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.permission;
+
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing projectTemplateId', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.projectTemplateId;
+
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for duplicated policy and projectTemplateId', (done) => {
+      models.WorkManagementPermission.create(body)
+        .then(() => {
+          request(server)
+            .post('/v5/projects/metadata/workManagementPermission')
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(body)
+            .expect('Content-Type', /json/)
+            .expect(400, done);
+        });
+    });
+
+    it('should return 400 for deleted but duplicated policy and projectTemplateId', (done) => {
+      models.WorkManagementPermission.create(body)
+        .then((permission) => {
+          models.WorkManagementPermission.destroy({ where: { id: permission.id } });
+        })
+        .then(() => {
+          request(server)
+            .post('/v5/projects/metadata/workManagementPermission')
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(body)
+            .expect('Content-Type', /json/)
+            .expect(400, done);
+        });
+    });
+
+    it('should return 201 for admin', (done) => {
+      request(server)
+        .post('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.policy.should.be.eql(body.policy);
+          resJson.permission.should.be.eql(body.permission);
+          resJson.projectTemplateId.should.be.eql(body.projectTemplateId);
+          resJson.createdBy.should.be.eql(40051333); // admin
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/workManagementPermissions/delete.js b/src/routes/workManagementPermissions/delete.js
new file mode 100644
index 0000000..1228918
--- /dev/null
+++ b/src/routes/workManagementPermissions/delete.js
@@ -0,0 +1,37 @@
+/**
+ * API to delete a work management permission
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workManagementPermission.delete'),
+  (req, res, next) =>
+     models.sequelize.transaction(() =>
+      models.WorkManagementPermission.findByPk(req.params.id)
+        .then((entity) => {
+          if (!entity) {
+            const apiErr = new Error(`Work Management Permission not found for id ${req.params.id}`);
+            apiErr.status = 404;
+            return Promise.reject(apiErr);
+          }
+          // Update the deletedBy, then delete
+          return entity.update({ deletedBy: req.authUser.userId });
+        })
+        .then(entity => entity.destroy()))
+        .then(() => {
+          res.status(204).end();
+        })
+        .catch(next),
+];
diff --git a/src/routes/workManagementPermissions/delete.spec.js b/src/routes/workManagementPermissions/delete.spec.js
new file mode 100644
index 0000000..9a24dcf
--- /dev/null
+++ b/src/routes/workManagementPermissions/delete.spec.js
@@ -0,0 +1,211 @@
+/**
+ * Tests for delete.js
+ */
+import _ from 'lodash';
+import request from 'supertest';
+import chai from 'chai';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const expectAfterDelete = (permissionId, err, next) => {
+  if (err) throw err;
+  setTimeout(() =>
+  models.WorkManagementPermission.findOne({
+    where: {
+      id: permissionId,
+    },
+    paranoid: false,
+  })
+    .then((res) => {
+      if (!res) {
+        throw new Error('Should found the entity');
+      } else {
+        chai.assert.isNotNull(res.deletedAt);
+        chai.assert.isNotNull(res.deletedBy);
+
+        request(server)
+          .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(404)
+          .end(next);
+      }
+    }), 500);
+};
+
+describe('DELETE work management permission', () => {
+  let permissionId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+
+  let permission = {
+    policy: 'work.create',
+    permission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+      },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'permissionId 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['permissionId-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((t) => {
+          permission = _.assign({}, permission, { projectTemplateId: t.id });
+          // Create projects
+          models.Project.create({
+            type: 'generic',
+            billingAccountId: 1,
+            name: 'test1',
+            description: 'test project1',
+            status: 'draft',
+            templateId: t.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then((project) => {
+            // create members
+            models.ProjectMember.bulkCreate([{
+              id: 1,
+              userId: copilotUser.userId,
+              projectId: project.id,
+              role: 'copilot',
+              isPrimary: false,
+              createdBy: 1,
+              updatedBy: 1,
+            }, {
+              id: 2,
+              userId: memberUser.userId,
+              projectId: project.id,
+              role: 'customer',
+              isPrimary: true,
+              createdBy: 1,
+              updatedBy: 1,
+            }]).then(() => {
+              models.WorkManagementPermission.create(permission)
+              .then((p) => {
+                permissionId = p.id;
+              })
+              .then(() => done());
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+
+  describe('DELETE /projects/metadata/workManagementPermission/{permissionId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed permission', (done) => {
+      request(server)
+        .delete('/v5/projects/metadata/workManagementPermission/123')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted permission', (done) => {
+      models.WorkManagementPermission.destroy({ where: { id: permissionId } })
+        .then(() => {
+          request(server)
+            .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 204, for admin, if permission was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(permissionId, err, done));
+    });
+
+    it('should return 204, for connect admin, if permission was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(permissionId, err, done));
+    });
+  });
+});
diff --git a/src/routes/workManagementPermissions/get.js b/src/routes/workManagementPermissions/get.js
new file mode 100644
index 0000000..eab1bb1
--- /dev/null
+++ b/src/routes/workManagementPermissions/get.js
@@ -0,0 +1,38 @@
+/**
+ * API to get a work management permission
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workManagementPermission.view'),
+  (req, res, next) => models.WorkManagementPermission.findOne({
+    where: {
+      id: req.params.id,
+    },
+    attributes: { exclude: ['deletedAt', 'deletedBy'] },
+  })
+    .then((existing) => {
+      // Not found
+      if (!existing) {
+        const apiErr = new Error(`Work Management Permission not found for id ${req.params.id}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+
+      res.json(existing);
+      return Promise.resolve();
+    })
+    .catch(next),
+];
diff --git a/src/routes/workManagementPermissions/get.spec.js b/src/routes/workManagementPermissions/get.spec.js
new file mode 100644
index 0000000..2945768
--- /dev/null
+++ b/src/routes/workManagementPermissions/get.spec.js
@@ -0,0 +1,149 @@
+/**
+ * Tests for get.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('GET work management permission', () => {
+  let permissionId;
+
+  let permission = {
+    policy: 'work.create',
+    permission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+      },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((t) => {
+          permission = _.assign({}, permission, { projectTemplateId: t.id });
+          models.WorkManagementPermission.create(permission)
+          .then((p) => {
+            permissionId = p.id;
+          })
+          .then(() => done());
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/metadata/workManagementPermission/{permissionId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-member', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed permission', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission/1234')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted permission', (done) => {
+      models.WorkManagementPermission.destroy({ where: { id: permissionId } })
+        .then(() => {
+          request(server)
+            .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 200 for admin', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(permissionId);
+          resJson.policy.should.be.eql(permission.policy);
+          resJson.permission.should.be.eql(permission.permission);
+          resJson.projectTemplateId.should.be.eql(permission.projectTemplateId);
+          resJson.createdBy.should.be.eql(permission.createdBy);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(permission.updatedBy);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/workManagementPermissions/list.js b/src/routes/workManagementPermissions/list.js
new file mode 100644
index 0000000..cef27bd
--- /dev/null
+++ b/src/routes/workManagementPermissions/list.js
@@ -0,0 +1,43 @@
+/**
+ * API to list all work management permissions
+ */
+import validate from 'express-validation';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import Joi from 'joi';
+import util from '../../util';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  query: {
+    filter: Joi.string().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workManagementPermission.view'),
+  (req, res, next) => {
+    // handle filters
+    const filters = util.parseQueryFilter(req.query.filter);
+    // Throw error if projectTemplateId is not present in filter
+    if (!filters.projectTemplateId) {
+      return next(util.buildApiError('Missing filter projectTemplateId', 400));
+    }
+    if (!util.isValidFilter(filters, ['projectTemplateId'])) {
+      return util.handleError('Invalid filters', null, req, next);
+    }
+    req.log.debug(filters);
+
+    return models.WorkManagementPermission.findAll({
+      where: filters,
+      attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      raw: true,
+    })
+    .then((result) => {
+      res.json(result);
+    })
+    .catch(next);
+  },
+];
diff --git a/src/routes/workManagementPermissions/list.spec.js b/src/routes/workManagementPermissions/list.spec.js
new file mode 100644
index 0000000..0a9728a
--- /dev/null
+++ b/src/routes/workManagementPermissions/list.spec.js
@@ -0,0 +1,241 @@
+/* eslint-disable max-len */
+/**
+ * Tests for list.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('LIST work management permissions', () => {
+  let templateIds;
+
+  const templates = [
+    {
+      name: 'template 1',
+      key: 'key 1',
+      category: 'category 1',
+      icon: 'http://example.com/icon1.ico',
+      question: 'question 1',
+      info: 'info 1',
+      aliases: ['key-1', 'key_1'],
+      disabled: true,
+      hidden: true,
+      scope: {
+        scope1: {
+          subScope1A: 1,
+          subScope1B: 2,
+        },
+        scope2: [1, 2, 3],
+      },
+      phases: {
+        phase1: {
+          name: 'phase 1',
+          details: {
+            anyDetails: 'any details 1',
+          },
+          others: ['others 11', 'others 12'],
+        },
+        phase2: {
+          name: 'phase 2',
+          details: {
+            anyDetails: 'any details 2',
+          },
+          others: ['others 21', 'others 22'],
+        },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      name: 'template 2',
+      key: 'key 2',
+      category: 'category 2',
+      icon: 'http://example.com/icon1.ico',
+      question: 'question 2',
+      info: 'info 2',
+      aliases: ['key-2', 'key_2'],
+      scope: {},
+      phases: {},
+      createdBy: 1,
+      updatedBy: 2,
+    },
+  ];
+  const permissions = [
+    {
+      policy: 'work.create',
+      permission: {
+        allowRule: {
+          projectRoles: ['customer', 'copilot'],
+          topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+        },
+        denyRule: { projectRoles: ['copilot'] },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      policy: 'work.edit',
+      permission: {
+        allowRule: {
+          projectRoles: ['customer', 'copilot'],
+          topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+        },
+        denyRule: { projectRoles: ['copilot'] },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ];
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.bulkCreate(templates, { returning: true })
+        .then((t) => {
+          templateIds = _.map(t, template => template.id);
+          const newPermissions = _.map(permissions, p => _.assign({}, p, { projectTemplateId: templateIds[0] }));
+          newPermissions.push(_.assign({}, permissions[0], { projectTemplateId: templateIds[1] }));
+          models.WorkManagementPermission.bulkCreate(newPermissions, { returning: true })
+            .then(() => done());
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/metadata/workManagementPermission', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1')
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-member', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D1')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 400 for missing filter', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing projectTemplateId', (done) => {
+      request(server)
+        .get('/v5/projects/metadata/workManagementPermission?filter=template')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for invalid filter', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission?filter=invalid%3D2%26projectTemplateId%3D${templateIds[0]}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(500, done);
+    });
+
+
+    it('should return 200 for admin for projectTemplateId=1', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D${templateIds[0]}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson[0].policy.should.be.eql(permissions[0].policy);
+          resJson[0].permission.should.be.eql(permissions[0].permission);
+          resJson[0].projectTemplateId.should.be.eql(templateIds[0]);
+          should.exist(resJson[0].createdAt);
+          resJson[0].updatedBy.should.be.eql(permissions[0].updatedBy);
+          should.exist(resJson[0].updatedAt);
+          should.not.exist(resJson[0].deletedBy);
+          should.not.exist(resJson[0].deletedAt);
+          resJson[1].policy.should.be.eql(permissions[1].policy);
+          resJson[1].permission.should.be.eql(permissions[1].permission);
+          resJson[1].projectTemplateId.should.be.eql(templateIds[0]);
+          should.exist(resJson[1].createdAt);
+          resJson[1].updatedBy.should.be.eql(permissions[1].updatedBy);
+          should.exist(resJson[1].updatedAt);
+          should.not.exist(resJson[1].deletedBy);
+          should.not.exist(resJson[1].deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin for projectTemplateId=2', (done) => {
+      request(server)
+        .get(`/v5/projects/metadata/workManagementPermission?filter=projectTemplateId%3D${templateIds[1]}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.length(1);
+          resJson[0].policy.should.be.eql(permissions[0].policy);
+          resJson[0].permission.should.be.eql(permissions[0].permission);
+          resJson[0].projectTemplateId.should.be.eql(templateIds[1]);
+          should.exist(resJson[0].createdAt);
+          resJson[0].updatedBy.should.be.eql(permissions[0].updatedBy);
+          should.exist(resJson[0].updatedAt);
+          should.not.exist(resJson[0].deletedBy);
+          should.not.exist(resJson[0].deletedAt);
+
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/workManagementPermissions/update.js b/src/routes/workManagementPermissions/update.js
new file mode 100644
index 0000000..7e18851
--- /dev/null
+++ b/src/routes/workManagementPermissions/update.js
@@ -0,0 +1,81 @@
+/* eslint-disable max-len */
+/**
+ * API to update a work management permission
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    id: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    policy: Joi.string().max(255).optional(),
+    permission: Joi.object().optional(),
+    projectTemplateId: Joi.number().integer().positive().optional(),
+    createdAt: Joi.any().strip(),
+    updatedAt: Joi.any().strip(),
+    deletedAt: Joi.any().strip(),
+    createdBy: Joi.any().strip(),
+    updatedBy: Joi.any().strip(),
+    deletedBy: Joi.any().strip(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workManagementPermission.edit'),
+  (req, res, next) => {
+    const entityToUpdate = _.assign(req.body, {
+      updatedBy: req.authUser.userId,
+    });
+
+    let permissionToUpdate;
+
+    return models.sequelize.transaction(() => // Get work management permission
+      models.WorkManagementPermission.findOne({
+        where: {
+          id: req.params.id,
+        },
+        attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      })
+      .then((permission) => {
+        // Not found
+        if (!permission) {
+          const apiErr = new Error(`Work Management Permission not found for id ${req.params.id}`);
+          apiErr.status = 404;
+          return Promise.reject(apiErr);
+        }
+
+        permissionToUpdate = permission;
+        return models.WorkManagementPermission.findOne({
+          where: {
+            policy: entityToUpdate.policy,
+            projectTemplateId: entityToUpdate.projectTemplateId,
+            id: { $ne: req.params.id },
+          },
+          paranoid: false,
+        });
+      })
+      .then((existing) => {
+        if (existing) {
+          const apiErr = new Error(`Work Management Permission already exists (may be deleted) for policy "${entityToUpdate.policy}" and project template id ${entityToUpdate.projectTemplateId}`);
+          apiErr.status = 400;
+          return Promise.reject(apiErr);
+        }
+
+        return permissionToUpdate.update(entityToUpdate);
+      }),
+    )
+      .then((updated) => {
+        res.json(updated);
+        return Promise.resolve();
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/workManagementPermissions/update.spec.js b/src/routes/workManagementPermissions/update.spec.js
new file mode 100644
index 0000000..cb16248
--- /dev/null
+++ b/src/routes/workManagementPermissions/update.spec.js
@@ -0,0 +1,248 @@
+/**
+ * Tests for update.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('UPDATE work management permission', () => {
+  let permissionId;
+  let templateId;
+
+  let permission = {
+    policy: 'work.create',
+    permission: {
+      allowRule: {
+        projectRoles: ['customer', 'copilot'],
+        topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+      },
+      denyRule: { projectRoles: ['copilot'] },
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((t) => {
+          templateId = t.id;
+          permission = _.assign({}, permission, { projectTemplateId: templateId });
+          models.WorkManagementPermission.create(permission)
+          .then((p) => {
+            permissionId = p.id;
+          })
+          .then(() => done());
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH /projects/metadata/workManagementPermission/{permissionId}', () => {
+    const body = {
+      policy: 'work.edit',
+      permission: {
+        allowRule: {
+          projectRoles: ['customer', 'copilot'],
+          topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+        },
+        denyRule: { projectRoles: ['copilot'] },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    };
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .send(body)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .send(body)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for non-member', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member2}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed type', (done) => {
+      request(server)
+        .patch('/v5/projects/metadata/workManagementPermission/1234')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(404)
+        .end(done);
+    });
+
+    it('should return 404 for deleted type', (done) => {
+      models.WorkManagementPermission.destroy({ where: { id: permissionId } })
+        .then(() => {
+          request(server)
+            .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(body)
+            .expect(404)
+            .end(done);
+        });
+    });
+
+    it('should return 400 when updated with invalid param', (done) => {
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({ invalid: null })
+        .expect('Content-Type', /json/)
+        .expect(400)
+        .end(done);
+    });
+
+    it('should return 400 for policy and projectTemplateId updated with existing(non-deleted) values', (done) => {
+      const newParam = _.assign({}, body, { projectTemplateId: templateId });
+      models.WorkManagementPermission.create(newParam)
+        .then(() => {
+          request(server)
+            .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(newParam)
+            .expect('Content-Type', /json/)
+            .expect(400)
+            .end(done);
+        });
+    });
+
+    it('should return 400 for policy and projectTemplateId updated with existing(deleted) values', (done) => {
+      const newParam = _.assign({}, body, { projectTemplateId: templateId });
+      models.WorkManagementPermission.create(newParam)
+        .then((p) => {
+          models.WorkManagementPermission.destroy({ where: { id: p.id } });
+        })
+        .then(() => {
+          request(server)
+            .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(newParam)
+            .expect('Content-Type', /json/)
+            .expect(400)
+            .end(done);
+        });
+    });
+
+    it('should return 200 for permission updated', (done) => {
+      const partialBody = _.assign({}, body, { projectTemplateId: templateId });
+      delete partialBody.permission;
+
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(partialBody)
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(permissionId);
+          resJson.policy.should.be.eql(partialBody.policy);
+          resJson.permission.should.be.eql(permission.permission);
+          resJson.projectTemplateId.should.be.eql(permission.projectTemplateId);
+          resJson.createdBy.should.be.eql(permission.createdBy); // should not update createdAt
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin all fields updated', (done) => {
+      const newParam = _.assign({}, body, { projectTemplateId: templateId });
+      request(server)
+        .patch(`/v5/projects/metadata/workManagementPermission/${permissionId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(newParam)
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(permissionId);
+          resJson.policy.should.be.eql(body.policy);
+          resJson.permission.should.be.eql(body.permission);
+          resJson.projectTemplateId.should.be.eql(newParam.projectTemplateId);
+          resJson.createdBy.should.be.eql(permission.createdBy); // should not update createdAt
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/workStreams/create.js b/src/routes/workStreams/create.js
new file mode 100644
index 0000000..bcd7221
--- /dev/null
+++ b/src/routes/workStreams/create.js
@@ -0,0 +1,70 @@
+/**
+ * API to add a work stream
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+import { WORKSTREAM_STATUS } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    name: Joi.string().max(255).required(),
+    type: Joi.string().max(45).required(),
+    status: Joi.string().valid(_.values(WORKSTREAM_STATUS)).required(),
+    createdAt: Joi.any().strip(),
+    updatedAt: Joi.any().strip(),
+    deletedAt: Joi.any().strip(),
+    createdBy: Joi.any().strip(),
+    updatedBy: Joi.any().strip(),
+    deletedBy: Joi.any().strip(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workStream.create'),
+  // do the real work
+  (req, res, next) => {
+    const data = req.body;
+    // default values
+    const projectId = _.parseInt(req.params.projectId);
+    _.assign(data, {
+      projectId,
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+    });
+
+    models.sequelize.transaction(() => {
+      req.log.debug('Create WorkStream - Starting transaction');
+      return models.Project.findOne({
+        where: { id: projectId, deletedAt: { $eq: null } },
+      })
+        .then((existingProject) => {
+          if (!existingProject) {
+            const err = new Error(`active project not found for project id ${projectId}`);
+            err.status = 404;
+            throw err;
+          }
+
+          return models.WorkStream.create(data);
+        })
+        .catch(next);
+    })
+      .then((createdEntity) => {
+        req.log.debug('new work stream created (id# %d, name: %s)',
+          createdEntity.id, createdEntity.name);
+        res.status(201).json(_.omit(createdEntity.toJSON(), 'deletedBy', 'deletedAt'));
+      })
+      .catch((err) => {
+        util.handleError('Error creating work stream', err, req, next);
+      });
+  },
+];
diff --git a/src/routes/workStreams/create.spec.js b/src/routes/workStreams/create.spec.js
new file mode 100644
index 0000000..4a66cdb
--- /dev/null
+++ b/src/routes/workStreams/create.spec.js
@@ -0,0 +1,255 @@
+/**
+ * Tests for create.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import server from '../../app';
+import testUtil from '../../tests/util';
+import models from '../../models';
+
+const should = chai.should();
+
+describe('CREATE work stream', () => {
+  const templates = [
+    {
+      name: 'template 1',
+      key: 'key 1',
+      category: 'category 1',
+      icon: 'http://example.com/icon1.ico',
+      question: 'question 1',
+      info: 'info 1',
+      aliases: ['key-1', 'key_1'],
+      disabled: true,
+      hidden: true,
+      scope: {
+        scope1: {
+          subScope1A: 1,
+          subScope1B: 2,
+        },
+        scope2: [1, 2, 3],
+      },
+      phases: {
+        phase1: {
+          name: 'phase 1',
+          details: {
+            anyDetails: 'any details 1',
+          },
+          others: ['others 11', 'others 12'],
+        },
+        phase2: {
+          name: 'phase 2',
+          details: {
+            anyDetails: 'any details 2',
+          },
+          others: ['others 21', 'others 22'],
+        },
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      name: 'template 2',
+      key: 'key 2',
+      category: 'category 2',
+      icon: 'http://example.com/icon1.ico',
+      question: 'question 2',
+      info: 'info 2',
+      aliases: ['key-2', 'key_2'],
+      scope: {},
+      phases: {},
+      createdBy: 1,
+      updatedBy: 2,
+    },
+  ];
+
+  let projectId;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.bulkCreate(templates, { returning: true })
+          .then((t) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: t[0].id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+            })
+              .then(() => done());
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('POST /projects/{projectId}/workstreams', () => {
+    const body = {
+      name: 'Work Stream',
+      type: 'generic',
+      status: 'active',
+    };
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed project id', (done) => {
+      request(server)
+        .delete('/v5/projects/999/workstreams')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted type', (done) => {
+      models.Project.destroy({ where: { id: projectId } })
+        .then(() => {
+          request(server)
+            .delete(`/v5/projects/${projectId}/workstreams`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 400 for missing type', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.type;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for missing name', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.name;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 for status', (done) => {
+      const invalidBody = _.cloneDeep(body);
+      delete invalidBody.status;
+
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(invalidBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 201 for admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.name.should.be.eql(body.name);
+          resJson.type.should.be.eql(body.type);
+          resJson.status.should.be.eql(body.status);
+          resJson.projectId.should.be.eql(projectId);
+
+          resJson.createdBy.should.be.eql(40051333);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(40051333);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 201 for connect admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.name.should.be.eql(body.name);
+          resJson.type.should.be.eql(body.type);
+          resJson.status.should.be.eql(body.status);
+          resJson.projectId.should.be.eql(projectId);
+          resJson.createdBy.should.be.eql(40051336);
+          resJson.updatedBy.should.be.eql(40051336);
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/workStreams/delete.js b/src/routes/workStreams/delete.js
new file mode 100644
index 0000000..aa2ef46
--- /dev/null
+++ b/src/routes/workStreams/delete.js
@@ -0,0 +1,44 @@
+/**
+ * API to delete a work stream
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workStream.delete'),
+  (req, res, next) =>
+     models.sequelize.transaction(() =>
+      models.WorkStream.findOne({
+        where: {
+          id: req.params.id,
+          projectId: req.params.projectId,
+        },
+      })
+        .then((entity) => {
+          if (!entity) {
+            const apiErr = new Error(`Work Stream not found for id ${req.params.id} ` +
+              `and project id ${req.params.projectId}`);
+            apiErr.status = 404;
+            return Promise.reject(apiErr);
+          }
+          // Update the deletedBy, then delete
+          return entity.update({ deletedBy: req.authUser.userId });
+        })
+        .then(entity => entity.destroy()))
+        .then(() => {
+          res.status(204).end();
+        })
+        .catch(next),
+];
diff --git a/src/routes/workStreams/delete.spec.js b/src/routes/workStreams/delete.spec.js
new file mode 100644
index 0000000..5a3b92b
--- /dev/null
+++ b/src/routes/workStreams/delete.spec.js
@@ -0,0 +1,168 @@
+/**
+ * Tests for delete.js
+ */
+import request from 'supertest';
+import chai from 'chai';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const expectAfterDelete = (id, projectId, err, next) => {
+  if (err) throw err;
+  setTimeout(() =>
+  models.WorkStream.findOne({
+    where: {
+      id,
+    },
+    paranoid: false,
+  })
+    .then((res) => {
+      if (!res) {
+        throw new Error('Should found the entity');
+      } else {
+        chai.assert.isNotNull(res.deletedAt);
+        chai.assert.isNotNull(res.deletedBy);
+
+        request(server)
+          .get(`/v5/projects/${projectId}/workstreams/${id}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(404, next);
+      }
+    }), 500);
+};
+
+describe('DELETE work stream', () => {
+  let projectId;
+  let id;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+          .then((template) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                id = entity.id;
+                done();
+              });
+            });
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('DELETE /projects/{projectId}/workstreams/{id}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed type', (done) => {
+      request(server)
+        .delete('/v5/projects/metadata/projectTypes/not_existed')
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted type', (done) => {
+      models.WorkStream.destroy({ where: { id } })
+        .then(() => {
+          request(server)
+            .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 204, for admin, if type was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(id, projectId, err, done));
+    });
+
+    it('should return 204, for connect admin, if type was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(id, projectId, err, done));
+    });
+  });
+});
diff --git a/src/routes/workStreams/get.js b/src/routes/workStreams/get.js
new file mode 100644
index 0000000..83ac929
--- /dev/null
+++ b/src/routes/workStreams/get.js
@@ -0,0 +1,41 @@
+/**
+ * API to get a work stream
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workStream.view'),
+  (req, res, next) => models.WorkStream.findOne({
+    where: {
+      id: req.params.id,
+      projectId: req.params.projectId,
+    },
+    attributes: { exclude: ['deletedAt', 'deletedBy'] },
+  })
+    .then((workStream) => {
+      // Not found
+      if (!workStream) {
+        const apiErr = new Error(`work stream not found for project id ${req.params.projectId} ` +
+          `and work stream id ${req.params.id}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+
+      res.json(workStream);
+      return Promise.resolve();
+    })
+    .catch(next),
+];
diff --git a/src/routes/workStreams/get.spec.js b/src/routes/workStreams/get.spec.js
new file mode 100644
index 0000000..d696da9
--- /dev/null
+++ b/src/routes/workStreams/get.spec.js
@@ -0,0 +1,162 @@
+/**
+ * Tests for get.js
+ */
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('GET work stream', () => {
+  let projectId;
+  let id;
+  let workStream;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+          .then((template) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                id = entity.id;
+                workStream = entity;
+                done();
+              });
+            });
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams/{id}', () => {
+    it('should return 404 for non-existed work stream', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/1234`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted work stream', (done) => {
+      models.WorkStream.destroy({ where: { id } })
+        .then(() => {
+          request(server)
+            .get(`/v5/projects/${projectId}/workstreams/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 200 for admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.name.should.be.eql(workStream.name);
+          resJson.type.should.be.eql(workStream.type);
+          resJson.status.should.be.eql(workStream.status);
+          resJson.projectId.should.be.eql(workStream.projectId);
+          resJson.createdBy.should.be.eql(workStream.createdBy);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(workStream.updatedBy);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 200 for connect admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 for connect manager', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+  });
+});
diff --git a/src/routes/workStreams/list.js b/src/routes/workStreams/list.js
new file mode 100644
index 0000000..c536e2b
--- /dev/null
+++ b/src/routes/workStreams/list.js
@@ -0,0 +1,45 @@
+/**
+ * API to list all work streams
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workStream.view'),
+  (req, res, next) => {
+    const projectId = req.params.projectId;
+    models.Project.count({
+      where: {
+        id: projectId,
+      },
+    })
+    .then((countProject) => {
+      if (countProject === 0) {
+        const apiErr = new Error(`active project not found for project id ${projectId}`);
+        apiErr.status = 404;
+        throw apiErr;
+      }
+
+      return models.WorkStream.findAll({
+        where: {
+          projectId,
+        },
+        attributes: { exclude: ['deletedAt', 'deletedBy'] },
+        raw: true,
+      });
+    })
+    .then(workStreams => res.json(workStreams))
+    .catch(next);
+  },
+];
diff --git a/src/routes/workStreams/list.spec.js b/src/routes/workStreams/list.spec.js
new file mode 100644
index 0000000..371ccc9
--- /dev/null
+++ b/src/routes/workStreams/list.spec.js
@@ -0,0 +1,157 @@
+/**
+ * Tests for list.js
+ */
+import chai from 'chai';
+import _ from 'lodash';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('LIST work streams', () => {
+  const workStreams = [{
+    name: 'Work Stream 1',
+    type: 'generic',
+    status: 'active',
+    createdBy: 1,
+    updatedBy: 1,
+  }, {
+    name: 'Work Stream 2',
+    type: 'generic',
+    status: 'reviewed',
+    createdBy: 1,
+    updatedBy: 1,
+  }];
+
+  let projectId;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+          .then((template) => {
+            // Create projects
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.bulkCreate(_.map(workStreams, w => _.assign(w, { projectId }))).then(() => done());
+            });
+          });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams', () => {
+    it('should return 200 for admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const workStream = workStreams[0];
+
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson[0].name.should.be.eql(workStream.name);
+          resJson[0].type.should.be.eql(workStream.type);
+          resJson[0].status.should.be.eql(workStream.status);
+          resJson[0].projectId.should.be.eql(workStream.projectId);
+          should.exist(resJson[0].createdAt);
+          resJson[0].updatedBy.should.be.eql(workStream.updatedBy);
+          should.exist(resJson[0].updatedAt);
+          should.not.exist(resJson[0].deletedBy);
+          should.not.exist(resJson[0].deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 404 for deleted project', (done) => {
+      models.Project.destroy({ where: { id: projectId } })
+        .then(() => {
+          request(server)
+            .get(`/v5/projects/${projectId}/workstreams`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 200 for connect admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 for connect manager', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+  });
+});
diff --git a/src/routes/workStreams/update.js b/src/routes/workStreams/update.js
new file mode 100644
index 0000000..e370c80
--- /dev/null
+++ b/src/routes/workStreams/update.js
@@ -0,0 +1,65 @@
+/**
+ * API to update a work stream
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+import { WORKSTREAM_STATUS } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    id: Joi.number().valid(Joi.ref('$params.id')),
+    name: Joi.string().max(255),
+    type: Joi.string().max(45),
+    status: Joi.string().valid(_.values(WORKSTREAM_STATUS)),
+    createdAt: Joi.any().strip(),
+    updatedAt: Joi.any().strip(),
+    deletedAt: Joi.any().strip(),
+    createdBy: Joi.any().strip(),
+    updatedBy: Joi.any().strip(),
+    deletedBy: Joi.any().strip(),
+  }).required(),
+};
+
+module.exports = [
+  validate(schema),
+  permissions('workStream.edit'),
+  (req, res, next) => {
+    const entityToUpdate = _.assign(req.body, {
+      updatedBy: req.authUser.userId,
+    });
+    const projectId = req.params.projectId;
+    const workStreamId = req.params.id;
+
+    return models.WorkStream.findOne({
+      where: {
+        id: workStreamId,
+        projectId,
+      },
+    })
+    .then((workStream) => {
+      if (!workStream) {
+        // handle 404
+        const err = new Error(`work stream not found for project id ${projectId} ` +
+          `and work stream id ${workStreamId}`);
+        err.status = 404;
+        return Promise.reject(err);
+      }
+
+      return workStream.update(entityToUpdate);
+    })
+      .then((workStream) => {
+        res.json(workStream);
+        return Promise.resolve();
+      })
+      .catch(next);
+  },
+];
diff --git a/src/routes/workStreams/update.spec.js b/src/routes/workStreams/update.spec.js
new file mode 100644
index 0000000..96c074f
--- /dev/null
+++ b/src/routes/workStreams/update.spec.js
@@ -0,0 +1,230 @@
+/**
+ * Tests for update.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('UPDATE Work Stream', () => {
+  let projectId;
+  let id;
+  let workStream;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'workStream.edit',
+            permission: {
+              allowRule: { projectRoles: ['manager', 'copilot'], topcoderRoles: ['Connect Admin', 'administrator'] },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create project
+            models.Project.create({
+              type: 'generic',
+              billingAccountId: 1,
+              name: 'test1',
+              description: 'test project1',
+              status: 'draft',
+              templateId: template.id,
+              details: {},
+              createdBy: 1,
+              updatedBy: 1,
+              lastActivityAt: 1,
+              lastActivityUserId: '1',
+            })
+            .then((project) => {
+              projectId = project.id;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                id = entity.id;
+                workStream = entity;
+                done();
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH /projects/{projectId}/workstreams/{id}', () => {
+    const body = {
+      name: 'Work Stream',
+      type: 'generic',
+      status: 'active',
+    };
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .send(body)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for manager', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .send(body)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed work stream', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/1234`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted work stream', (done) => {
+      models.WorkStream.destroy({ where: { id } })
+        .then(() => {
+          request(server)
+            .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .send(body)
+            .expect(404, done);
+        });
+    });
+
+    it('should return 200 for admin name updated', (done) => {
+      const partialBody = _.cloneDeep(body);
+      delete partialBody.type;
+      delete partialBody.status;
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(partialBody)
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(id);
+          resJson.name.should.be.eql(workStream.name);
+          resJson.type.should.be.eql(workStream.type);
+          resJson.status.should.be.eql(workStream.status);
+          resJson.projectId.should.be.eql(workStream.projectId);
+          resJson.createdBy.should.be.eql(workStream.createdBy);
+          resJson.createdBy.should.be.eql(workStream.createdBy);
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 200 for admin all fields updated', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(body)
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(id);
+          resJson.name.should.be.eql(workStream.name);
+          resJson.type.should.be.eql(workStream.type);
+          resJson.status.should.be.eql(workStream.status);
+          resJson.projectId.should.be.eql(workStream.projectId);
+          resJson.createdBy.should.be.eql(workStream.createdBy);
+          resJson.updatedBy.should.be.eql(40051333); // admin
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 200 for connect admin', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${id}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(body)
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.id.should.be.eql(id);
+          resJson.name.should.be.eql(workStream.name);
+          resJson.type.should.be.eql(workStream.type);
+          resJson.status.should.be.eql(workStream.status);
+          resJson.projectId.should.be.eql(workStream.projectId);
+          resJson.createdBy.should.be.eql(workStream.createdBy);
+          resJson.updatedBy.should.be.eql(40051336); // connect admin
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/works/create.js b/src/routes/works/create.js
new file mode 100644
index 0000000..a1d3cf5
--- /dev/null
+++ b/src/routes/works/create.js
@@ -0,0 +1,159 @@
+/**
+ * API to add a phase as work
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import Sequelize from 'sequelize';
+
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES } from '../../constants';
+
+const permissions = require('tc-core-library-js').middleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    name: Joi.string().required(),
+    description: Joi.string().optional(),
+    requirements: Joi.string().optional(),
+    status: Joi.string().required(),
+    startDate: Joi.date().optional(),
+    endDate: Joi.date().optional(),
+    duration: Joi.number().min(0).optional(),
+    budget: Joi.number().min(0).optional(),
+    spentBudget: Joi.number().min(0).optional(),
+    progress: Joi.number().min(0).optional(),
+    details: Joi.any().optional(),
+    order: Joi.number().integer().optional(),
+    productTemplateId: Joi.number().integer().positive().optional(),
+  }).required(),
+};
+
+module.exports = [
+  // validate request payload
+  validate(schema),
+  // check permission
+  permissions('work.create'),
+  // do the real work
+  (req, res, next) => {
+    // default values
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+
+    const data = req.body;
+    _.assign(data, {
+      projectId,
+      createdBy: req.authUser.userId,
+      updatedBy: req.authUser.userId,
+    });
+
+    let existingWorkStream = null;
+    let newProjectPhase = null;
+
+    req.log.debug('Create Work - Starting transaction');
+    return models.sequelize.transaction(() =>
+      models.WorkStream.findOne({
+        where: {
+          id: workStreamId,
+          projectId,
+          deletedAt: { $eq: null },
+        },
+      })
+        .then((_existingWorkStream) => {
+          if (!_existingWorkStream) {
+            // handle 404
+            const err = new Error(`active work stream not found for project id ${projectId} ` +
+              `and work stream id ${workStreamId}`);
+            err.status = 404;
+            throw err;
+          }
+
+          existingWorkStream = _existingWorkStream;
+
+          if (data.startDate !== null && data.endDate !== null && data.startDate > data.endDate) {
+            const err = new Error('startDate must not be after endDate.');
+            err.status = 400;
+            throw err;
+          }
+          return models.ProjectPhase.create(data);
+        })
+        .then((_newProjectPhase) => {
+          newProjectPhase = _.omit(_newProjectPhase.toJSON(), ['deletedAt', 'deletedBy']);
+          return existingWorkStream.addProjectPhase(_newProjectPhase.id);
+        })
+        .then(() => {
+          req.log.debug('re-ordering the other phases');
+
+          if (_.isNil(newProjectPhase.order)) {
+            return Promise.resolve();
+          }
+          // Increase the order of the other phases in the same project,
+          // which have `order` >= this phase order
+          return models.ProjectPhase.update({ order: Sequelize.literal('"order" + 1') }, {
+            where: {
+              projectId,
+              id: { $ne: newProjectPhase.id },
+              order: { $gte: newProjectPhase.order },
+            },
+          });
+        })
+        .then(() => {
+          if (_.isNil(data.productTemplateId)) {
+            return Promise.resolve();
+          }
+
+          // Get the product template
+          return models.ProductTemplate.findByPk(data.productTemplateId)
+            .then((productTemplate) => {
+              if (!productTemplate) {
+                const err = new Error(`Product template does not exist with id = ${data.productTemplateId}`);
+                err.status = 400;
+                throw err;
+              }
+
+              // Create the phase product
+              return models.PhaseProduct.create({
+                name: productTemplate.name,
+                templateId: data.productTemplateId,
+                type: productTemplate.productKey,
+                projectId,
+                phaseId: newProjectPhase.id,
+                createdBy: req.authUser.userId,
+                updatedBy: req.authUser.userId,
+              })
+                .then((phaseProduct) => {
+                  newProjectPhase.products = [
+                    _.omit(phaseProduct.toJSON(), ['deletedAt', 'deletedBy']),
+                  ];
+                });
+            });
+        }),
+    )
+    .then(() => {
+      // Send events to buses
+      req.log.debug('Sending event to RabbitMQ bus for project phase %d', newProjectPhase.id);
+      req.app.services.pubsub.publish(EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED,
+        { added: newProjectPhase, route: TIMELINE_REFERENCES.WORK },
+        { correlationId: req.id },
+      );
+
+      req.log.debug('Sending event to Kafka bus for project phase %d', newProjectPhase.id);
+      util.sendResourceToKafkaBus(
+        req,
+        EVENT.ROUTING_KEY.PROJECT_PHASE_ADDED,
+        RESOURCES.PHASE,
+        newProjectPhase,
+      );
+
+      res.status(201).json(newProjectPhase);
+    })
+    .catch((err) => {
+      util.handleError('Error creating work', err, req, next);
+    });
+  },
+];
diff --git a/src/routes/works/create.spec.js b/src/routes/works/create.spec.js
new file mode 100644
index 0000000..52fe870
--- /dev/null
+++ b/src/routes/works/create.spec.js
@@ -0,0 +1,374 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for create.js
+ */
+
+import _ from 'lodash';
+import chai from 'chai';
+import sinon from 'sinon';
+import request from 'supertest';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+import { BUS_API_EVENT, CONNECT_NOTIFICATION_EVENT, RESOURCES } from '../../constants';
+
+const should = chai.should();
+
+const validatePhase = (resJson, expectedPhase) => {
+  should.exist(resJson);
+  resJson.name.should.be.eql(expectedPhase.name);
+  resJson.status.should.be.eql(expectedPhase.status);
+  resJson.budget.should.be.eql(expectedPhase.budget);
+  resJson.progress.should.be.eql(expectedPhase.progress);
+  resJson.details.should.be.eql(expectedPhase.details);
+};
+
+describe('CREATE work', () => {
+  let projectId;
+  let projectName;
+  let workStreamId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
+  let productTemplateId;
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'work.create',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create(_.assign(project, { templateId: template.id }))
+            .then((_project) => {
+              projectId = _project.id;
+              projectName = _project.name;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                // create members
+                models.ProjectMember.bulkCreate([{
+                  id: 1,
+                  userId: copilotUser.userId,
+                  projectId,
+                  role: 'copilot',
+                  isPrimary: false,
+                  createdBy: 1,
+                  updatedBy: 1,
+                }, {
+                  id: 2,
+                  userId: memberUser.userId,
+                  projectId,
+                  role: 'customer',
+                  isPrimary: true,
+                  createdBy: 1,
+                  updatedBy: 1,
+                }])
+                .then(() =>
+                  models.ProductTemplate.create({
+                    name: 'name 1',
+                    productKey: 'productKey 1',
+                    category: 'generic',
+                    subCategory: 'generic',
+                    icon: 'http://example.com/icon1.ico',
+                    brief: 'brief 1',
+                    details: 'details 1',
+                    aliases: ['product key 1', 'product_key_1'],
+                    template: {
+                      template1: {
+                        name: 'template 1',
+                        details: {
+                          anyDetails: 'any details 1',
+                        },
+                        others: ['others 11', 'others 12'],
+                      },
+                      template2: {
+                        name: 'template 2',
+                        details: {
+                          anyDetails: 'any details 2',
+                        },
+                        others: ['others 21', 'others 22'],
+                      },
+                    },
+                    createdBy: 1,
+                    updatedBy: 2,
+                  }).then((productTemplate) => {
+                    productTemplateId = productTemplate.id;
+                    done();
+                  }),
+                );
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH /projects/{projectId}/workstreams/{workStreamId}/works', () => {
+    const body = {
+      name: 'test project phase',
+      description: 'test project phase description',
+      requirements: 'test project phase requirements',
+      status: 'active',
+      startDate: '2018-05-15T00:00:00Z',
+      endDate: '2018-05-15T12:00:00Z',
+      budget: 20.0,
+      progress: 1.23456,
+      spentBudget: 10.0,
+      duration: 10,
+      details: {
+        message: 'This can be any json',
+      },
+    };
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .send(body)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .send(body)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 for non-existed work stream', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/1234/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(body)
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted work stream', (done) => {
+      models.WorkStream.destroy({ where: { id: workStreamId } })
+        .then(() => {
+          request(server)
+            .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.manager}`,
+            })
+            .send(body)
+            .expect(404, done);
+        });
+    });
+
+    it('should return 400 when name not provided', (done) => {
+      const reqBody = _.cloneDeep(body);
+      delete reqBody.name;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(reqBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when status not provided', (done) => {
+      const reqBody = _.cloneDeep(body);
+      delete reqBody.status;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(reqBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when startDate > endDate', (done) => {
+      const reqBody = _.cloneDeep(body);
+      reqBody.startDate = '2018-05-16T12:00:00';
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(reqBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when budget is negative', (done) => {
+      const reqBody = _.cloneDeep(body);
+      reqBody.budget = -20;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(reqBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when progress is negative', (done) => {
+      const reqBody = _.cloneDeep(body);
+      reqBody.progress = -20;
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(reqBody)
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 201 for member', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(_.assign({ productTemplateId }, body))
+        .expect(201, done);
+    });
+
+    it('should return 201 for connect admin', (done) => {
+      request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .send(_.assign({ productTemplateId }, body))
+        .expect(201)
+        .end((err, res) => {
+          const resJson = res.body;
+          validatePhase(resJson, body);
+          done();
+        });
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when work added', (done) => {
+        request(server)
+        .post(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(body)
+        .expect('Content-Type', /json/)
+        .expect(201)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_CREATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                name: body.name,
+                status: body.status,
+                budget: body.budget,
+                progress: body.progress,
+                projectId,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051331,
+                initiatorUserId: 40051331,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+  });
+});
diff --git a/src/routes/works/delete.js b/src/routes/works/delete.js
new file mode 100644
index 0000000..885c8c5
--- /dev/null
+++ b/src/routes/works/delete.js
@@ -0,0 +1,81 @@
+/**
+ * API to delete a work
+ */
+import _ from 'lodash';
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('work.delete'),
+  (req, res, next) => {
+    const projectId = req.params.projectId;
+    models.sequelize.transaction(() =>
+    models.PhaseWorkStream.findOne({
+      where: {
+        phaseId: req.params.id,
+        workStreamId: req.params.workStreamId,
+      },
+    })
+    .then((work) => {
+      // Not found
+      if (!work) {
+        const apiErr = new Error(`work not found for work stream id ${req.params.workStreamId} ` +
+          `and work id ${req.params.id}`);
+        apiErr.status = 404;
+        throw apiErr;
+      }
+
+      return models.ProjectPhase.findOne({
+        where: {
+          id: req.params.id,
+          projectId,
+        },
+      });
+    })
+    .then((entity) => {
+      if (!entity) {
+        const apiErr = new Error(`work not found for work stream id ${req.params.workStreamId}, ` +
+          `project id ${projectId} and work id ${req.params.id}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+      // Update the deletedBy, then delete
+      return entity.update({ deletedBy: req.authUser.userId });
+    })
+    .then(entity => entity.destroy()))
+    .then((deleted) => {
+      req.log.debug('deleted work', JSON.stringify(deleted, null, 2));
+
+      // Send events to buses
+      req.app.services.pubsub.publish(
+        EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED,
+        { deleted, route: TIMELINE_REFERENCES.WORK },
+        { correlationId: req.id },
+      );
+
+      //  emit event
+      util.sendResourceToKafkaBus(
+        req,
+        EVENT.ROUTING_KEY.PROJECT_PHASE_REMOVED,
+        RESOURCES.PHASE,
+        _.pick(deleted.toJSON(), 'id'));
+
+      res.status(204).json({});
+    }).catch(err => next(err));
+  },
+];
diff --git a/src/routes/works/delete.spec.js b/src/routes/works/delete.spec.js
new file mode 100644
index 0000000..b57c25c
--- /dev/null
+++ b/src/routes/works/delete.spec.js
@@ -0,0 +1,304 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for delete.js
+ */
+import _ from 'lodash';
+import request from 'supertest';
+import chai from 'chai';
+import sinon from 'sinon';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+import { BUS_API_EVENT, CONNECT_NOTIFICATION_EVENT, RESOURCES } from '../../constants';
+
+chai.should();
+
+const expectAfterDelete = (workId, projectId, workStreamId, err, next) => {
+  if (err) throw err;
+  setTimeout(() =>
+  models.ProjectPhase.findOne({
+    where: {
+      id: workId,
+    },
+    paranoid: false,
+  })
+    .then((res) => {
+      if (!res) {
+        throw new Error('Should found the entity');
+      } else {
+        chai.assert.isNotNull(res.deletedAt);
+        chai.assert.isNotNull(res.deletedBy);
+
+        request(server)
+          .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+          .set({
+            Authorization: `Bearer ${testUtil.jwts.admin}`,
+          })
+          .expect(404, next);
+      }
+    }), 500);
+};
+
+describe('DELETE work', () => {
+  let projectId;
+  let projectName;
+  let workStreamId;
+  let workId;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'work.delete',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create(_.assign(project, { templateId: template.id }))
+            .then((_project) => {
+              projectId = _project.id;
+              projectName = _project.name;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                models.ProjectPhase.create({
+                  name: 'test project phase',
+                  status: 'active',
+                  startDate: '2018-05-15T00:00:00Z',
+                  endDate: '2018-05-15T12:00:00Z',
+                  budget: 20.0,
+                  progress: 1.23456,
+                  details: {
+                    message: 'This can be any json',
+                  },
+                  createdBy: 1,
+                  updatedBy: 1,
+                  projectId,
+                }).then((phase) => {
+                  workId = phase.id;
+                  models.PhaseWorkStream.create({
+                    phaseId: workId,
+                    workStreamId,
+                  }).then(() => {
+                    // create members
+                    models.ProjectMember.bulkCreate([{
+                      id: 1,
+                      userId: copilotUser.userId,
+                      projectId,
+                      role: 'copilot',
+                      isPrimary: false,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }, {
+                      id: 2,
+                      userId: memberUser.userId,
+                      projectId,
+                      role: 'customer',
+                      isPrimary: true,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }]).then(() => done());
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('DELETE /projects/{projectId}/workstreams/{workStreamId}/works/{workId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/999/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted type', (done) => {
+      models.ProjectPhase.destroy({ where: { id: workId } })
+        .then(() => {
+          request(server)
+            .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 204 for member', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(workId, projectId, workStreamId, err, done));
+    });
+
+    it('should return 204, for admin, if type was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(workId, projectId, workStreamId, err, done));
+    });
+
+    it('should return 204, for connect admin, if type was successfully removed', (done) => {
+      request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(204)
+        .end(err => expectAfterDelete(workId, projectId, workStreamId, err, done));
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when work removed', (done) => {
+        request(server)
+        .delete(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(204)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_DELETED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051336,
+                initiatorUserId: 40051336,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+  });
+});
diff --git a/src/routes/works/get.js b/src/routes/works/get.js
new file mode 100644
index 0000000..b94b974
--- /dev/null
+++ b/src/routes/works/get.js
@@ -0,0 +1,58 @@
+/**
+ * API to get a work
+ */
+import validate from 'express-validation';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('work.view'),
+  (req, res, next) => models.PhaseWorkStream.findOne({
+    where: {
+      phaseId: req.params.id,
+      workStreamId: req.params.workStreamId,
+    },
+  //  attributes: { exclude: ['deletedAt', 'deletedBy'] },
+  })
+    .then((work) => {
+      // Not found
+      if (!work) {
+        const apiErr = new Error(`work not found for work stream id ${req.params.workStreamId}, ` +
+          `project id ${req.params.projectId} and work id ${req.params.id}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+
+      return models.ProjectPhase.findOne({
+        where: {
+          id: req.params.id,
+          projectId: req.params.projectId,
+        },
+        attributes: { exclude: ['deletedAt', 'deletedBy'] },
+      });
+    })
+    .then((phase) => {
+      // Not found
+      if (!phase) {
+        const apiErr = new Error(`work not found for work stream id ${req.params.workStreamId}, ` +
+          `project id ${req.params.projectId} and work id ${req.params.id}`);
+        apiErr.status = 404;
+        return Promise.reject(apiErr);
+      }
+
+      return res.json(phase);
+    })
+    .catch(next),
+];
diff --git a/src/routes/works/get.spec.js b/src/routes/works/get.spec.js
new file mode 100644
index 0000000..768fd78
--- /dev/null
+++ b/src/routes/works/get.spec.js
@@ -0,0 +1,216 @@
+/**
+ * Tests for get.js
+ */
+import chai from 'chai';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('GET work', () => {
+  const body = {
+    name: 'test project phase',
+    status: 'active',
+    startDate: '2018-05-15T00:00:00Z',
+    endDate: '2018-05-15T12:00:00Z',
+    budget: 20.0,
+    progress: 1.23456,
+    details: {
+      message: 'This can be any json',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+  let projectId;
+  let workStreamId;
+  let workId;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          // Create projects
+          models.Project.create({
+            type: 'generic',
+            billingAccountId: 1,
+            name: 'test1',
+            description: 'test project1',
+            status: 'draft',
+            templateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then((project) => {
+            projectId = project.id;
+            models.WorkStream.create({
+              name: 'Work Stream',
+              type: 'generic',
+              status: 'active',
+              projectId,
+              createdBy: 1,
+              updatedBy: 1,
+            }).then((entity) => {
+              workStreamId = entity.id;
+              models.ProjectPhase.create({
+                name: 'test project phase',
+                status: 'active',
+                startDate: '2018-05-15T00:00:00Z',
+                endDate: '2018-05-15T12:00:00Z',
+                budget: 20.0,
+                progress: 1.23456,
+                details: {
+                  message: 'This can be any json',
+                },
+                createdBy: 1,
+                updatedBy: 1,
+                projectId,
+              }).then((phase) => {
+                workId = phase.id;
+                models.PhaseWorkStream.create({
+                  phaseId: workId,
+                  workStreamId,
+                }).then(() => done());
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams/{workStreamId}/works/{workId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 404 when no project with specific projectId', (done) => {
+      request(server)
+        .get(`/v5/projects/9999/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/999/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 for deleted type', (done) => {
+      models.ProjectPhase.destroy({ where: { id: workId } })
+        .then(() => {
+          request(server)
+            .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+            .set({
+              Authorization: `Bearer ${testUtil.jwts.admin}`,
+            })
+            .expect(404, done);
+        });
+    });
+
+    it('should return 200 for admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.name.should.be.eql(body.name);
+          resJson.status.should.be.eql(body.status);
+          resJson.budget.should.be.eql(body.budget);
+          resJson.progress.should.be.eql(body.progress);
+          resJson.details.should.be.eql(body.details);
+          resJson.createdBy.should.be.eql(body.createdBy);
+          should.exist(resJson.createdAt);
+          resJson.updatedBy.should.be.eql(body.updatedBy);
+          should.exist(resJson.updatedAt);
+          should.not.exist(resJson.deletedBy);
+          should.not.exist(resJson.deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 200 for connect admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 for connect manager', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+  });
+});
diff --git a/src/routes/works/list.js b/src/routes/works/list.js
new file mode 100644
index 0000000..78f00f0
--- /dev/null
+++ b/src/routes/works/list.js
@@ -0,0 +1,96 @@
+/**
+ * API to list all works
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import util from '../../util';
+import models from '../../models';
+
+const PHASE_ATTRIBUTES = _.keys(models.ProjectPhase.rawAttributes);
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+  },
+};
+
+module.exports = [
+  validate(schema),
+  permissions('work.view'),
+  (req, res, next) => {
+    const workStreamId = req.params.workStreamId;
+    const projectId = req.params.projectId;
+
+    // Parse the fields string to determine what fields are to be returned
+    const rawFields = req.query.fields ? decodeURIComponent(req.query.fields).split(',') : PHASE_ATTRIBUTES;
+    let sort = req.query.sort ? decodeURIComponent(req.query.sort) : 'startDate';
+    if (sort && sort.indexOf(' ') === -1) {
+      sort += ' asc';
+    }
+    const sortableProps = [
+      'startDate asc', 'startDate desc',
+      'endDate asc', 'endDate desc',
+      'status asc', 'status desc',
+      'order asc', 'order desc',
+    ];
+    if (sort && _.indexOf(sortableProps, sort) < 0) {
+      return util.handleError('Invalid sort criteria', null, req, next);
+    }
+
+    const sortParameters = sort.split(' ');
+
+    const fields = _.union(
+      _.intersection(rawFields, [...PHASE_ATTRIBUTES, 'workItems']),
+      ['id'], // required fields
+    );
+
+    // search condition for ProjectPhase
+    const include = {
+      model: models.ProjectPhase,
+      through: { attributes: [] },
+      where: {
+        projectId,
+      },
+      attributes: fields.filter(f => f !== 'workItems'),
+      required: false,
+    };
+    if (fields.includes('workItems')) {
+      _.set(include, 'include', [{ model: models.PhaseProduct, as: 'products' }]);
+    }
+
+    return models.WorkStream.findOne({
+      where: {
+        id: workStreamId,
+        projectId,
+        deletedAt: { $eq: null },
+      },
+      include: [include],
+      order: [[models.ProjectPhase, sortParameters[0], sortParameters[1]]],
+    })
+    .then((existingWorkStream) => {
+      if (!existingWorkStream) {
+        // handle 404
+        const err = new Error(`active work stream not found for project id ${projectId} ` +
+          `and work stream id ${workStreamId}`);
+        err.status = 404;
+        throw err;
+      }
+
+      // rename 'products' to 'workItems'
+      return existingWorkStream.ProjectPhases.map((phase) => {
+        const phaseObj = phase.get({ plain: true });
+        if (_.has(phaseObj, 'products')) {
+          _.set(phaseObj, 'workItems', _.get(phaseObj, 'products'));
+          _.unset(phaseObj, 'products');
+        }
+        return phaseObj;
+      });
+    })
+    .then(phases => res.json(phases))
+    .catch(next);
+  },
+];
diff --git a/src/routes/works/list.spec.js b/src/routes/works/list.spec.js
new file mode 100644
index 0000000..4edc179
--- /dev/null
+++ b/src/routes/works/list.spec.js
@@ -0,0 +1,224 @@
+/**
+ * Tests for list.js
+ */
+import chai from 'chai';
+import _ from 'lodash';
+import request from 'supertest';
+
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+
+const should = chai.should();
+
+describe('LIST works', () => {
+  const phases = [
+    {
+      name: 'test project phase',
+      status: 'active',
+      startDate: '2018-05-15T00:00:00Z',
+      endDate: '2018-05-15T12:00:00Z',
+      budget: 20.0,
+      progress: 1.23456,
+      details: {
+        message: 'This can be any json',
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+    {
+      name: 'test project phase',
+      status: 'active',
+      startDate: '2018-05-15T00:00:00Z',
+      endDate: '2018-05-15T12:00:00Z',
+      budget: 20.0,
+      progress: 1.23456,
+      details: {
+        message: 'This can be any json',
+      },
+      createdBy: 1,
+      updatedBy: 1,
+    },
+  ];
+
+  const productBody = {
+    name: 'test phase product',
+    type: 'product1',
+    estimatedPrice: 20.0,
+    actualPrice: 1.23456,
+    details: {
+      message: 'This can be any json',
+    },
+    createdBy: 1,
+    updatedBy: 1,
+  };
+
+
+  let projectId;
+  let workStreamId;
+
+  beforeEach((done) => {
+    testUtil.clearDb()
+        .then(() => {
+          models.ProjectTemplate.create({
+            name: 'template 2',
+            key: 'key 2',
+            category: 'category 2',
+            icon: 'http://example.com/icon1.ico',
+            question: 'question 2',
+            info: 'info 2',
+            aliases: ['key-2', 'key_2'],
+            scope: {},
+            phases: {},
+            createdBy: 1,
+            updatedBy: 2,
+          })
+            .then((template) => {
+              // Create projects
+              models.Project.create({
+                type: 'generic',
+                billingAccountId: 1,
+                name: 'test1',
+                description: 'test project1',
+                status: 'draft',
+                templateId: template.id,
+                details: {},
+                createdBy: 1,
+                updatedBy: 1,
+                lastActivityAt: 1,
+                lastActivityUserId: '1',
+              })
+              .then((project) => {
+                projectId = project.id;
+                models.WorkStream.create({
+                  name: 'Work Stream',
+                  type: 'generic',
+                  status: 'active',
+                  projectId,
+                  createdBy: 1,
+                  updatedBy: 1,
+                }).then((entity) => {
+                  workStreamId = entity.id;
+                  models.ProjectPhase.bulkCreate(_.map(phases, p => _.assign(p, { projectId })),
+                    { returning: true })
+                    .then((p) => {
+                      models.PhaseWorkStream.bulkCreate([{
+                        phaseId: p[0].id,
+                        workStreamId,
+                      }, {
+                        phaseId: p[1].id,
+                        workStreamId,
+                      }]).then((ws) => {
+                        _.assign(productBody, { phaseId: ws[0].phaseId, projectId });
+
+                        models.PhaseProduct.create(productBody).then(() => {
+                          done();
+                        });
+                      });
+                    });
+                });
+              });
+            });
+        });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('GET /projects/{projectId}/workstreams/{workStreamId}/works', () => {
+    it('should return 200 for admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const phase = phases[0];
+
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson[0].name.should.be.eql(phase.name);
+          resJson[0].status.should.be.eql(phase.status);
+          resJson[0].budget.should.be.eql(phase.budget);
+          resJson[0].progress.should.be.eql(phase.progress);
+          resJson[0].details.should.be.eql(phase.details);
+          should.exist(resJson[0].createdAt);
+          resJson[0].updatedBy.should.be.eql(phase.updatedBy);
+          should.exist(resJson[0].updatedAt);
+          should.not.exist(resJson[0].deletedBy);
+          should.not.exist(resJson[0].deletedAt);
+
+          done();
+        });
+    });
+
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .expect(403, done);
+    });
+
+    it('should return 403 for member', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .expect(403, done);
+    });
+
+    it('should return 200 for connect admin', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.connectAdmin}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return 200 for connect manager', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .expect(200)
+        .end(done);
+    });
+
+    it('should return with populated workItems if fields=workItems is used', (done) => {
+      request(server)
+        .get(`/v5/projects/${projectId}/workstreams/${workStreamId}/works?fields=workItems`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .expect(200)
+        .end((err, res) => {
+          const resJson = res.body;
+          resJson.should.have.length(2);
+          resJson[0].should.have.property('workItems');
+          resJson[0].workItems.should.be.a('array');
+          resJson[0].workItems.should.have.lengthOf(1);
+          resJson[0].workItems[0].should.have.property('projectId');
+          resJson[0].workItems[0].projectId.should.equal(projectId);
+          resJson[0].workItems[0].name.should.equal(productBody.name);
+          resJson[1].should.have.property('workItems');
+          resJson[1].workItems.should.be.a('array');
+          resJson[1].workItems.should.have.lengthOf(0);
+          done();
+        });
+    });
+  });
+});
diff --git a/src/routes/works/update.js b/src/routes/works/update.js
new file mode 100644
index 0000000..0dd343a
--- /dev/null
+++ b/src/routes/works/update.js
@@ -0,0 +1,194 @@
+/**
+ * API to update work
+ */
+import validate from 'express-validation';
+import _ from 'lodash';
+import Joi from 'joi';
+import Sequelize from 'sequelize';
+import { middleware as tcMiddleware } from 'tc-core-library-js';
+import models from '../../models';
+import util from '../../util';
+import { EVENT, RESOURCES, TIMELINE_REFERENCES, ROUTES } from '../../constants';
+
+const permissions = tcMiddleware.permissions;
+
+const schema = {
+  params: {
+    projectId: Joi.number().integer().positive().required(),
+    workStreamId: Joi.number().integer().positive().required(),
+    id: Joi.number().integer().positive().required(),
+  },
+  body: Joi.object().keys({
+    name: Joi.string().optional(),
+    description: Joi.string().optional(),
+    requirements: Joi.string().optional(),
+    status: Joi.string().optional(),
+    startDate: Joi.date().optional(),
+    endDate: Joi.date().optional(),
+    duration: Joi.number().min(0).optional(),
+    budget: Joi.number().min(0).optional(),
+    spentBudget: Joi.number().min(0).optional(),
+    progress: Joi.number().min(0).optional(),
+    details: Joi.any().optional(),
+    order: Joi.number().integer().optional(),
+  }).required(),
+};
+
+
+module.exports = [
+  // validate request payload
+  validate(schema),
+  // check permission
+  permissions('work.edit'),
+
+  (req, res, next) => {
+    const projectId = _.parseInt(req.params.projectId);
+    const workStreamId = _.parseInt(req.params.workStreamId);
+    const phaseId = _.parseInt(req.params.id);
+
+    const updatedProps = req.body;
+    updatedProps.updatedBy = req.authUser.userId;
+
+    let previousValue;
+    let updated;
+
+    models.sequelize.transaction(() => models.ProjectPhase.findOne({
+      where: {
+        id: phaseId,
+        projectId,
+      },
+      include: [{
+        model: models.WorkStream,
+        through: { attributes: [] },
+        where: {
+          id: workStreamId,
+          projectId,
+        },
+      }],
+    })
+    .then((existing) => {
+      if (!existing) {
+        // handle 404
+        const err = new Error('No active project phase found for project id ' +
+          `${projectId} and work stream ${workStreamId} and phase id ${phaseId}`);
+        err.status = 404;
+        throw err;
+      } else {
+        previousValue = _.clone(existing.get({ plain: true }));
+
+        // make sure startDate < endDate
+        let startDate;
+        let endDate;
+        if (updatedProps.startDate) {
+          startDate = new Date(updatedProps.startDate);
+        } else {
+          startDate = existing.startDate !== null ? new Date(existing.startDate) : null;
+        }
+
+        if (updatedProps.endDate) {
+          endDate = new Date(updatedProps.endDate);
+        } else {
+          endDate = existing.endDate !== null ? new Date(existing.endDate) : null;
+        }
+
+        if (startDate !== null && endDate !== null && startDate > endDate) {
+          const err = new Error('startDate must not be after endDate.');
+          err.status = 400;
+          throw err;
+        } else {
+          _.extend(existing, updatedProps);
+          return existing.save().catch(next);
+        }
+      }
+    })
+      .then((updatedPhase) => {
+        updated = updatedPhase;
+        // Ignore re-ordering if there's no order specified for this phase
+        if (_.isNil(updated.order)) {
+          return Promise.resolve();
+        }
+
+        // Update order of the other phases only if the order was changed
+        if (previousValue.order === updated.order) {
+          return Promise.resolve();
+        }
+
+        return models.ProjectPhase.count({
+          where: {
+            id: { $ne: phaseId },
+          },
+          include: [{
+            model: models.WorkStream,
+            where: {
+              id: workStreamId,
+              projectId,
+            },
+          }],
+        })
+          .then((count) => {
+            if (count === 0) {
+              return Promise.resolve();
+            }
+
+            // Increase the order from M to K: if there is an item with order K,
+            // orders from M+1 to K should be made M to K-1
+            if (!_.isNil(previousValue.order) && previousValue.order < updated.order) {
+              return models.ProjectPhase.update({ order: Sequelize.literal('"order" - 1') }, {
+                where: {
+                  projectId,
+                  id: { $ne: updated.id },
+                  order: { $between: [previousValue.order + 1, updated.order] },
+                },
+              });
+            }
+
+            // Decrease the order from M to K: if there is an item with order K,
+            // orders from K to M-1 should be made K+1 to M
+            return models.ProjectPhase.update({ order: Sequelize.literal('"order" + 1') }, {
+              where: {
+                projectId,
+                id: { $ne: updated.id },
+                order: {
+                  $between: [
+                    updated.order,
+                    (previousValue.order ? previousValue.order : Number.MAX_SAFE_INTEGER) - 1,
+                  ],
+                },
+              },
+            });
+          });
+      })
+      .then(() =>
+        // To simpify the logic, reload the phases from DB and send to the message queue
+        models.ProjectPhase.findAll({
+          where: {
+            projectId,
+          },
+          include: [{ model: models.PhaseProduct, as: 'products' }],
+        })),
+    )
+      .then((allPhases) => {
+        req.log.debug('updated project phase', JSON.stringify(updated, null, 2));
+
+        const updatedValue = updated.get({ plain: true });
+
+        // emit original and updated project phase information
+        req.app.services.pubsub.publish(
+          EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
+          { original: previousValue, updated: updatedValue, allPhases, route: TIMELINE_REFERENCES.WORK },
+          { correlationId: req.id },
+        );
+        util.sendResourceToKafkaBus(
+          req,
+          EVENT.ROUTING_KEY.PROJECT_PHASE_UPDATED,
+          RESOURCES.PHASE,
+          updatedValue,
+          previousValue,
+          ROUTES.WORKS.UPDATE,
+        );
+
+        res.json(updated);
+      })
+      .catch(err => next(err));
+  },
+];
diff --git a/src/routes/works/update.spec.js b/src/routes/works/update.spec.js
new file mode 100644
index 0000000..9528669
--- /dev/null
+++ b/src/routes/works/update.spec.js
@@ -0,0 +1,780 @@
+/* eslint-disable no-unused-expressions */
+/**
+ * Tests for update.js
+ */
+import _ from 'lodash';
+import chai from 'chai';
+import request from 'supertest';
+import sinon from 'sinon';
+import models from '../../models';
+import server from '../../app';
+import testUtil from '../../tests/util';
+import busApi from '../../services/busApi';
+import { BUS_API_EVENT, RESOURCES, CONNECT_NOTIFICATION_EVENT } from '../../constants';
+
+const should = chai.should();
+
+const body = {
+  name: 'test project phase',
+  description: 'test project phase description',
+  requirements: 'test project phase requirements',
+  status: 'active',
+  startDate: '2018-05-15T00:00:00Z',
+  endDate: '2018-05-15T12:00:00Z',
+  budget: 20.0,
+  progress: 1.23456,
+  details: {
+    message: 'This can be any json',
+  },
+  createdBy: 1,
+  updatedBy: 1,
+};
+
+const updateBody = {
+  name: 'test project phase xxx',
+  description: 'test project phase description xxx',
+  requirements: 'test project phase requirements xxx',
+  status: 'inactive',
+  startDate: '2018-05-11T00:00:00Z',
+  endDate: '2018-05-12T12:00:00Z',
+  budget: 123456.789,
+  progress: 9.8765432,
+  details: {
+    message: 'This is another json',
+  },
+};
+
+const validatePhase = (resJson, expectedPhase) => {
+  should.exist(resJson);
+  resJson.name.should.be.eql(expectedPhase.name);
+  resJson.status.should.be.eql(expectedPhase.status);
+  resJson.budget.should.be.eql(expectedPhase.budget);
+  resJson.progress.should.be.eql(expectedPhase.progress);
+  resJson.details.should.be.eql(expectedPhase.details);
+};
+
+describe('UPDATE work', () => {
+  let projectId;
+  let projectName;
+  let workStreamId;
+  let workId;
+  let workId2;
+  let workId3;
+
+  const memberUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.member).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.member).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const copilotUser = {
+    handle: testUtil.getDecodedToken(testUtil.jwts.copilot).handle,
+    userId: testUtil.getDecodedToken(testUtil.jwts.copilot).userId,
+    firstName: 'fname',
+    lastName: 'lName',
+    email: 'some@abc.com',
+  };
+  const project = {
+    type: 'generic',
+    billingAccountId: 1,
+    name: 'test1',
+    description: 'test project1',
+    status: 'draft',
+    details: {},
+    createdBy: 1,
+    updatedBy: 1,
+    lastActivityAt: 1,
+    lastActivityUserId: '1',
+  };
+  beforeEach((done) => {
+    testUtil.clearDb()
+      .then(() => {
+        models.ProjectTemplate.create({
+          name: 'template 2',
+          key: 'key 2',
+          category: 'category 2',
+          icon: 'http://example.com/icon1.ico',
+          question: 'question 2',
+          info: 'info 2',
+          aliases: ['key-2', 'key_2'],
+          scope: {},
+          phases: {},
+          createdBy: 1,
+          updatedBy: 2,
+        })
+        .then((template) => {
+          models.WorkManagementPermission.create({
+            policy: 'work.edit',
+            permission: {
+              allowRule: {
+                projectRoles: ['customer', 'copilot'],
+                topcoderRoles: ['Connect Manager', 'Connect Admin', 'administrator'],
+              },
+              denyRule: { projectRoles: ['copilot'] },
+            },
+            projectTemplateId: template.id,
+            details: {},
+            createdBy: 1,
+            updatedBy: 1,
+            lastActivityAt: 1,
+            lastActivityUserId: '1',
+          })
+          .then(() => {
+            // Create projects
+            models.Project.create(_.assign(project, { templateId: template.id }))
+            .then((_project) => {
+              projectId = _project.id;
+              projectName = _project.name;
+              models.WorkStream.create({
+                name: 'Work Stream',
+                type: 'generic',
+                status: 'active',
+                projectId,
+                createdBy: 1,
+                updatedBy: 1,
+              }).then((entity) => {
+                workStreamId = entity.id;
+                _.assign(body, { projectId });
+                const createPhases = [
+                  body,
+                  _.assign({ order: 1 }, body),
+                  _.assign({}, body, { status: 'draft' }),
+                ];
+                models.ProjectPhase.bulkCreate(createPhases, { returning: true }).then((phases) => {
+                  workId = phases[0].id;
+                  workId2 = phases[1].id;
+                  workId3 = phases[2].id;
+                  models.PhaseWorkStream.bulkCreate([{
+                    phaseId: phases[0].id,
+                    workStreamId,
+                  }, {
+                    phaseId: phases[1].id,
+                    workStreamId,
+                  }, {
+                    phaseId: phases[2].id,
+                    workStreamId,
+                  }]).then(() => {
+                    // create members
+                    models.ProjectMember.bulkCreate([{
+                      id: 1,
+                      userId: copilotUser.userId,
+                      projectId,
+                      role: 'copilot',
+                      isPrimary: false,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }, {
+                      id: 2,
+                      userId: memberUser.userId,
+                      projectId,
+                      role: 'customer',
+                      isPrimary: true,
+                      createdBy: 1,
+                      updatedBy: 1,
+                    }]).then(() => done());
+                  });
+                });
+              });
+            });
+          });
+        });
+      });
+  });
+
+  after((done) => {
+    testUtil.clearDb(done);
+  });
+
+  describe('PATCH /projects/{projectId}/workstreams/{workStreamId}/works/{workId}', () => {
+    it('should return 403 if user is not authenticated', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .send(updateBody)
+        .expect(403, done);
+    });
+
+    it('should return 403 for copilot', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.copilot}`,
+        })
+        .send(updateBody)
+        .expect(403, done);
+    });
+
+    it('should return 404 when no work stream with specific workStreamId', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/999/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 404 when no work with specific workId', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/999`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(404, done);
+    });
+
+    it('should return 400 when parameters are invalid', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({
+          progress: -15,
+        })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 400 when startDate > endDate', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.manager}`,
+        })
+        .send({
+          endDate: '2018-05-13T00:00:00Z',
+        })
+        .expect('Content-Type', /json/)
+        .expect(400, done);
+    });
+
+    it('should return 200 for member', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.member}`,
+        })
+        .send(updateBody)
+        .expect(200, done);
+    });
+
+    it('should return updated phase when user have permission and parameters are valid', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(updateBody)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            validatePhase(resJson, updateBody);
+            done();
+          }
+        });
+    });
+
+    it('should return updated phase when parameters are valid (0 for non -ve numbers)', (done) => {
+      const bodyWithZeros = _.cloneDeep(updateBody);
+      bodyWithZeros.duration = 0;
+      bodyWithZeros.spentBudget = 0.0;
+      bodyWithZeros.budget = 0.0;
+      bodyWithZeros.progress = 0.0;
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(bodyWithZeros)
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            validatePhase(resJson, bodyWithZeros);
+            done();
+          }
+        });
+    });
+
+    it('should return updated phase if the order is specified', (done) => {
+      request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send(_.assign({ order: 1 }, updateBody))
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err, res) => {
+          if (err) {
+            done(err);
+          } else {
+            const resJson = res.body;
+            validatePhase(resJson, updateBody);
+            resJson.order.should.be.eql(1);
+
+            // Check the order of the other phase
+            models.ProjectPhase.findOne({ where: { id: workId2 } })
+              .then((work2) => {
+                work2.order.should.be.eql(2);
+                done();
+              });
+          }
+        });
+    });
+
+    describe('Bus api', () => {
+      let createEventSpy;
+      const sandbox = sinon.sandbox.create();
+
+
+      before((done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+
+      beforeEach(() => {
+        createEventSpy = sandbox.spy(busApi, 'createEvent');
+      });
+
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      it('should send correct BUS API messages when spentBudget updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          spentBudget: 123,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_PAYMENT).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when progress updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          progress: 50,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(3);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_PROGRESS).should.be.true;
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PROGRESS_MODIFIED).should.be.true;
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when details updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          details: {
+            text: 'something',
+          },
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_UPDATE_SCOPE).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when status updated (completed)', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          status: 'completed',
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_TRANSITION_COMPLETED).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when status updated (active)', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId3}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          status: 'active',
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId3,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_WORK_TRANSITION_ACTIVE).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when budget updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          budget: 123,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when startDate updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          startDate: 123,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+
+      it('should send correct BUS API messages when duration updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          duration: 100,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(2);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                duration: 100,
+              })).should.be.true;
+
+              // Check Notification Service events
+              createEventSpy.calledWith(CONNECT_NOTIFICATION_EVENT.PROJECT_PLAN_UPDATED, sinon.match({
+                projectId,
+                projectName,
+                projectUrl: `https://local.topcoder-dev.com/projects/${projectId}`,
+                userId: 40051333,
+                initiatorUserId: 40051333,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when order updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          order: 100,
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              // NOTE: no other event should be called, as this phase doesn't move any other phases
+
+              done();
+            });
+          }
+        });
+      });
+
+      it('should send correct BUS API messages when endDate updated', (done) => {
+        request(server)
+        .patch(`/v5/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({
+          endDate: new Date(),
+        })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              createEventSpy.callCount.should.be.eql(1);
+
+              createEventSpy.calledWith(BUS_API_EVENT.PROJECT_PHASE_UPDATED, sinon.match({
+                resource: RESOURCES.PHASE,
+                id: workId,
+                updatedBy: testUtil.userIds.admin,
+              })).should.be.true;
+
+              done();
+            });
+          }
+        });
+      });
+    });
+
+    /* describe('RabbitMQ Message topic', () => {
+      let updateMessageSpy;
+      let publishSpy;
+      let sandbox;
+
+      before(async (done) => {
+        // Wait for 500ms in order to wait for createEvent calls from previous tests to complete
+        testUtil.wait(done);
+      });
+
+      beforeEach(async (done) => {
+        sandbox = sinon.sandbox.create();
+        server.services.pubsub = new RabbitMQService(server.logger);
+
+        // initialize RabbitMQ
+        server.services.pubsub.init(
+          config.get('rabbitmqURL'),
+          config.get('pubsubExchangeName'),
+          config.get('pubsubQueueName'),
+        );
+
+        // add project to ES index
+        await server.services.es.index({
+          index: ES_PROJECT_INDEX,
+          type: ES_PROJECT_TYPE,
+          id: projectId,
+          body: {
+            doc: _.assign(project, { phases: [_.assign(body, { id: workId, projectId })] }),
+          },
+        });
+
+        testUtil.wait(() => {
+          publishSpy = sandbox.spy(server.services.pubsub, 'publish');
+          updateMessageSpy = sandbox.spy(messageService, 'updateTopic');
+          sandbox.stub(messageService, 'getTopicByTag', () => Promise.resolve(topic));
+          done();
+        });
+      });
+
+      afterEach(() => {
+        sandbox.restore();
+      });
+
+      after(() => {
+        mockRabbitMQ(server);
+      });
+
+      it('should send message topic when work updated', (done) => {
+        const mockHttpClient = _.merge(testUtil.mockHttpClient, {
+          post: () => Promise.resolve({
+            status: 200,
+            data: {
+              id: 'requesterId',
+              version: 'v3',
+              result: {
+                success: true,
+                status: 200,
+                content: {},
+              },
+            },
+          }),
+        });
+        sandbox.stub(messageService, 'getClient', () => mockHttpClient);
+        request(server)
+        .patch(`/v4/projects/${projectId}/workstreams/${workStreamId}/works/${workId}`)
+        .set({
+          Authorization: `Bearer ${testUtil.jwts.admin}`,
+        })
+        .send({ param: _.assign(updateBody, { budget: 123 }) })
+        .expect('Content-Type', /json/)
+        .expect(200)
+        .end((err) => {
+          if (err) {
+            done(err);
+          } else {
+            testUtil.wait(() => {
+              publishSpy.calledOnce.should.be.true;
+              publishSpy.calledWith('project.phase.updated').should.be.true;
+              updateMessageSpy.calledTwice.should.be.true;
+              done();
+            });
+          }
+        });
+      });
+    }); */
+  });
+});
diff --git a/src/services/messageService.js b/src/services/messageService.js
index 949c013..c596ddd 100644
--- a/src/services/messageService.js
+++ b/src/services/messageService.js
@@ -1,6 +1,5 @@
 import config from 'config';
 import _ from 'lodash';
-// import util from '../util';
 
 const Promise = require('bluebird');
 const axios = require('axios');
@@ -141,19 +140,19 @@ function deletePosts(topicId, postIds, logger) {
  * Fetches the topic of given phase of the project.
  *
  * @param {Integer} projectId id of the project
- * @param {Integer} phaseId id of the phase of the project
+ * @param {String} tag tag
  * @param {Object} logger object
  * @return {Promise} topic promise
  */
-function getPhaseTopic(projectId, phaseId, logger) {
-  logger.debug(`getPhaseTopic for projectId: ${projectId} phaseId: ${phaseId}`);
+function getTopicByTag(projectId, tag, logger) {
+  logger.debug(`getTopicByTag for projectId: ${projectId} tag: ${tag}`);
   return getClient(logger).then((msgClient) => {
-    logger.debug(`calling message service for fetching phaseId#${phaseId}`);
-    const encodedFilter = encodeURIComponent(`reference=project&referenceId=${projectId}&tag=phase#${phaseId}`);
+    logger.debug(`calling message service for fetching ${tag}`);
+    const encodedFilter = encodeURIComponent(`reference=project&referenceId=${projectId}&tag=${tag}`);
     return msgClient.get(`/topics/list/db?filter=${encodedFilter}`)
     .then((resp) => {
-      logger.debug('Fetched phase topic', resp);
       const topics = _.get(resp.data, 'result.content', []);
+      logger.debug(`Fetched ${topics.length} topics`);
       if (topics && topics.length > 0) {
         return topics[0];
       }
@@ -181,6 +180,7 @@ module.exports = {
   createTopic,
   updateTopic,
   deletePosts,
-  getPhaseTopic,
+  getTopicByTag,
   deleteTopic,
+  getClient,
 };
diff --git a/src/tests/mockRabbitMQ.js b/src/tests/mockRabbitMQ.js
new file mode 100644
index 0000000..3913bd7
--- /dev/null
+++ b/src/tests/mockRabbitMQ.js
@@ -0,0 +1,18 @@
+/**
+ * Mock RabbitMQ service
+ */
+/* globals Promise */
+
+import sinon from 'sinon';
+import _ from 'lodash';
+
+module.exports = (app) => {
+  _.assign(app.services, {
+    pubsub: {
+      init: () => {},
+      publish: () => {},
+    },
+  });
+  sinon.stub(app.services.pubsub, 'init', () => Promise.resolve(true));
+  sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true));
+};
diff --git a/src/tests/serviceMocks.js b/src/tests/serviceMocks.js
index 662bd2c..060596d 100644
--- a/src/tests/serviceMocks.js
+++ b/src/tests/serviceMocks.js
@@ -7,16 +7,13 @@ import _ from 'lodash';
 import config from 'config';
 import elasticsearch from 'elasticsearch';
 import util from '../util';
+import mockRabbitMQ from './mockRabbitMQ';
 
 module.exports = (app) => {
+  mockRabbitMQ(app);
+
   _.assign(app.services, {
-    pubsub: {
-      init: () => {},
-      publish: () => {},
-    },
     es: new elasticsearch.Client(_.cloneDeep(config.elasticsearchConfig)),
   });
-  sinon.stub(app.services.pubsub, 'init', () => Promise.resolve(true));
-  sinon.stub(app.services.pubsub, 'publish', () => Promise.resolve(true));
   sinon.stub(util, 'getM2MToken', () => Promise.resolve('MOCK_TOKEN'));
 };
diff --git a/src/tests/util.js b/src/tests/util.js
index 3031dd1..08f0a65 100644
--- a/src/tests/util.js
+++ b/src/tests/util.js
@@ -1,6 +1,7 @@
 /* eslint-disable max-len */
 
 import models from '../models';
+import elasticsearchSync from '../../migrations/elasticsearch_sync';
 
 const jwt = require('jsonwebtoken');
 
@@ -9,6 +10,10 @@ export default {
       .then(() => {
         if (done) done();
       }),
+  clearES: done => elasticsearchSync.sync()
+      .then(() => {
+        if (done) done();
+      }),
   mockHttpClient: {
     defaults: { headers: { common: {} } },
     interceptors: { response: { use: () => {} } },
@@ -26,6 +31,8 @@ export default {
     member2: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJtZW1iZXIyIiwiZXhwIjoyNTYzMDc2Njg5LCJ1c2VySWQiOiI0MDA1MTMzNSIsImlhdCI6MTQ2MzA3NjA4OSwiZW1haWwiOiJ0ZXN0QHRvcGNvZGVyLmNvbSIsImp0aSI6ImIzM2I3N2NkLWI1MmUtNDBmZS04MzdlLWJlYjhlMGFlNmE0YSJ9.Mh4bw3wm-cn5Kcf96gLFVlD0kySOqqk4xN3qnreAKL4',
     // userId = 40051336, [ 'Connect Admin' ], handle: 'connect_admin1', email: 'connect_admin1@topcoder.com'
     connectAdmin: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJDb25uZWN0IEFkbWluIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJjb25uZWN0X2FkbWluMSIsImV4cCI6MjU2MzA3NjY4OSwidXNlcklkIjoiNDAwNTEzMzYiLCJpYXQiOjE0NjMwNzYwODksImVtYWlsIjoiY29ubmVjdF9hZG1pbjFAdG9wY29kZXIuY29tIiwianRpIjoiYjMzYjc3Y2QtYjUyZS00MGZlLTgzN2UtYmViOGUwYWU2YTRhIn0.nSGfXMl02NZ90ZKLiEKPg75iAjU92mfteaY6xgqkM30',
+    // userId = 40158431, [ 'Topcoder user' ], handle: 'romitchoudhary', email: 'romit.choudhary@rivigo.com'
+    romit: 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJyb2xlcyI6WyJUb3Bjb2RlciBVc2VyIl0sImlzcyI6Imh0dHBzOi8vYXBpLnRvcGNvZGVyLWRldi5jb20iLCJoYW5kbGUiOiJyb21pdGNob3VkaGFyeSIsImV4cCI6MTU2MjkxOTc5MSwidXNlcklkIjoiNDAxNTg0MzEiLCJpYXQiOjE1NjI5MTkxOTEsImVtYWlsIjoicm9taXQuY2hvdWRoYXJ5QHJpdmlnby5jb20iLCJqdGkiOiJlMmM1ZTc2NS03OTI5LTRiNzgtYjI2OS1iZDRlODA0NDI4YjMifQ.P1CoydCJuQ8Hv_b0-a8V7Wu0pgIt9qv4NYyB7FTbua0',
   },
   userIds: {
     member: 40051331,
@@ -34,6 +41,7 @@ export default {
     manager: 40051334,
     member2: 40051335,
     connectAdmin: 40051336,
+    romit: 40158431,
   },
   getDecodedToken: token => jwt.decode(token),
 
diff --git a/src/util.js b/src/util.js
index da991b5..b17fdeb 100644
--- a/src/util.js
+++ b/src/util.js
@@ -11,22 +11,33 @@
 
 
 import _ from 'lodash';
+import querystring from 'querystring';
 import config from 'config';
 import urlencode from 'urlencode';
 import elasticsearch from 'elasticsearch';
 import Promise from 'bluebird';
+import models from './models';
 // import AWS from 'aws-sdk';
 
-import { ADMIN_ROLES, TOKEN_SCOPES, EVENT, PROJECT_MEMBER_ROLE } from './constants';
+import {
+  ADMIN_ROLES,
+  TOKEN_SCOPES,
+  EVENT,
+  PROJECT_MEMBER_ROLE,
+  VALUE_TYPE,
+  ESTIMATION_TYPE,
+  RESOURCES,
+} from './constants';
 
 const exec = require('child_process').exec;
-const models = require('./models').default;
 const tcCoreLibAuth = require('tc-core-library-js').auth;
 
 const m2m = tcCoreLibAuth.m2m(config);
 
 const util = _.cloneDeep(require('tc-core-library-js').util(config));
 
+const ssoRefCodes = JSON.parse(config.get('SSO_REFCODES'));
+
 // the client modifies the config object, so always passed the cloned object
 let esClient = null;
 
@@ -77,6 +88,76 @@ _.assignIn(util, {
     });
     return valid;
   },
+  /**
+   * Calculate project estimation item price
+   * @param  {object}   valueType       value type can be int, string, double, percentage
+   * @param  {String}   value           value
+   * @param  {Double}   price           price
+   * @return {Double|String}            calculated price value
+   */
+  calculateEstimationItemPrice: (valueType, value, price) => {
+    if (valueType === VALUE_TYPE.PERCENTAGE) {
+      return (value * price) / 100;
+    }
+    return value;
+  },
+  /**
+   * Calculate project estimation item price
+   * @param   {Object}    req               the request
+   * @param  {Number}     projectId         project id
+   * @return {Array}  estimation items
+   */
+  calculateProjectEstimationItems: (req, projectId) =>
+    // delete ALL existent ProjectEstimationItems for the project
+     models.ProjectEstimationItem.deleteAllForProject(models, projectId, req.authUser, {
+       includeAllProjectEstimatinoItemsForInternalUsage: true,
+     })
+
+      // retrieve ProjectSettings and ProjectEstimations
+      .then(() => Promise.all([
+        models.ProjectSetting.findAll({
+          includeAllProjectSettingsForInternalUsage: true,
+          where: {
+            projectId,
+            key: _.map(_.values(ESTIMATION_TYPE), type => `markup_${type}`),
+          },
+          raw: true,
+        }),
+        models.ProjectEstimation.findAll({
+          where: { projectId: req.params.projectId },
+          raw: true,
+        }),
+      ]))
+
+      // create ProjectEstimationItems
+      .then(([settings, estimations]) => {
+        if (!settings || settings.length === 0) {
+          req.log.debug('No project settings for prices found, therefore no estimation items are created');
+          return [];
+        }
+
+        if (!estimations || estimations.length === 0) {
+          req.log.debug('No price estimations found, therefore no estimation items are created');
+          return [];
+        }
+
+        const estimationItems = [];
+        _.each(estimations, (estimation) => {
+          _.each(settings, (setting) => {
+            estimationItems.push({
+              projectEstimationId: estimation.id,
+              price: util.calculateEstimationItemPrice(setting.valueType, setting.value, estimation.price),
+              type: setting.key.replace(/^markup_/, ''),
+              markupUsedReference: 'projectSetting',
+              markupUsedReferenceId: setting.id,
+              createdBy: req.authUser.userId,
+              updatedBy: req.authUser.userId,
+            });
+          });
+        });
+
+        return models.ProjectEstimationItem.bulkCreate(estimationItems);
+      }),
   /**
    * Helper funtion to verify if user has specified role
    * @param  {object} req  Request object that should contain authUser
@@ -165,6 +246,26 @@ _.assignIn(util, {
     return fields;
   },
 
+  /**
+   * Parse the query filters
+   * @param  {String}   fqueryFilter        the query filter string
+   * @return {Object}                       the parsed array
+   */
+  parseQueryFilter: (fqueryFilter) => {
+    let queryFilter = querystring.parse(fqueryFilter);
+    // convert in to array
+    queryFilter = _.mapValues(queryFilter, (val) => {
+      if (val.indexOf('in(') > -1) {
+        return { $in: val.substring(3, val.length - 1).split(',') };
+      }
+      return val;
+    });
+    if (queryFilter.id && queryFilter.id.$in) {
+      queryFilter.id.$in = _.map(queryFilter.id.$in, _.parseInt);
+    }
+    return queryFilter;
+  },
+
   /**
    * Moves file from source to destination
    * @param  {object} req    request object
@@ -296,6 +397,16 @@ _.assignIn(util, {
     } else {
       esClient = new elasticsearch.Client(_.cloneDeep(config.elasticsearchConfig));
     }
+    // during unit tests, we need to refresh the indices
+    // before making get/search requests to make sure all ES data can be visible.
+    if (process.env.NODE_ENV.toLowerCase() === 'test') {
+      esClient.originalSearch = esClient.search;
+      esClient.search = (params, cb) => esClient.indices.refresh({ index: '' })
+        .then(() => esClient.originalSearch(params, cb)); // refresh index before reply
+      esClient.originalGet = esClient.get;
+      esClient.get = (params, cb) => esClient.indices.refresh({ index: '' })
+        .then(() => esClient.originalGet(params, cb)); // refresh index before reply
+    }
     return esClient;
   },
 
@@ -432,11 +543,21 @@ _.assignIn(util, {
      * @param  {String} key  the event key
      * @param  {String} name  the resource name
      * @param  {object} resource  the resource
+     * @param  {object} [originalResource] original resource in case resource was updated
+     * @param  {String} [route] route which called the event (for phases and works)
+     * @param  {Boolean}[skipNotification] if true, than event is not send to Notification Service
     */
-    sendResourceToKafkaBus: Promise.coroutine(function* (req, key, name, resource) {    // eslint-disable-line
+    sendResourceToKafkaBus: Promise.coroutine(function* (req, key, name, resource, originalResource, route, skipNotification) {    // eslint-disable-line
       req.log.debug('Sending event to Kafka bus for resource %s %s', name, resource.id || resource.key);
+
       // emit event
-      req.app.emit(key, { req, resource: _.assign({ resource: name }, resource) });
+      req.app.emit(key, {
+        req,
+        resource: _.assign({ resource: name }, resource),
+        originalResource: originalResource ? _.assign({ resource: name }, originalResource) : undefined,
+        route,
+        skipNotification,
+      });
     }),
 
   /**
@@ -468,6 +589,12 @@ _.assignIn(util, {
         newMember,
         { correlationId: req.id },
       );
+      // emit the event
+      util.sendResourceToKafkaBus(
+        req,
+        EVENT.ROUTING_KEY.PROJECT_MEMBER_ADDED,
+        RESOURCES.PROJECT_MEMBER,
+        newMember);
 
       return newMember;
     })
@@ -517,6 +644,72 @@ _.assignIn(util, {
     });
   },
 
+  /**
+   * Lookup user handles from multiple emails
+   * @param {Object}  req        request
+   * @param {Array}   userEmails user emails
+   * @param {Number} maximumRequests  limit number of request on one batch
+   * @param {Boolean} isPattern  flag to indicate that pattern matching is required or not
+   * @return {Promise} promise
+   */
+  lookupMultipleUserEmails(req, userEmails, maximumRequests, isPattern = false) {
+    req.log.debug(`identityServiceEndpoint: ${config.get('identityServiceEndpoint')}`);
+
+    const httpClient = util.getHttpClient({ id: req.id, log: req.log });
+    // request generator function
+    const generateRequest = ({ token, email }) => {
+      let filter = `email=${email}`;
+      if (isPattern) {
+        filter += '&like=true';
+      }
+      return httpClient.get(`${config.get('identityServiceEndpoint')}users`, {
+        headers: {
+          Authorization: `Bearer ${token}`,
+          Accept: 'application/json',
+          'Content-Type': 'application/json',
+        },
+        params: {
+          fields: 'handle,id,email',
+          filter,
+        },
+        // set longer timeout as default 3000 could be not enough for identity service response
+        timeout: 15000,
+      }).catch(() => {
+        // in case of any error happens during getting user by email
+        // we treat such users as not found and don't return error
+        // as per discussion in issue #334
+      });
+    };
+    // send batch of requests, one batch at one time
+    const sendBatch = (options) => {
+      const token = options.token;
+      const emails = options.emails;
+      const users = options.users || [];
+      const batch = options.batch || 0;
+      const start = batch * maximumRequests;
+      const end = (batch + 1) * maximumRequests;
+      const requests = emails.slice(start, end).map(userEmail =>
+          generateRequest({ token, email: userEmail }));
+      return Promise.all(requests)
+        .then((responses) => {
+          const data = responses.reduce((contents, response) => {
+            const content = _.get(response, 'data.result.content', []);
+            return _.concat(contents, content);
+          }, users);
+          req.log.debug(`UserHandle response batch-${batch}`, data);
+          if (end < emails.length) {
+            return sendBatch({ token, users: data, emails, batch: batch + 1 });
+          }
+          return data;
+        });
+    };
+    return util.getM2MToken()
+      .then((m2mToken) => {
+        req.log.debug(`Bearer ${m2mToken}`);
+        return sendBatch({ token: m2mToken, emails: userEmails });
+      });
+  },
+
   /**
    * Filter only members of topcoder team
    * @param {Array}  members        project members
@@ -524,6 +717,13 @@ _.assignIn(util, {
    */
   getTopcoderProjectMembers: members => _(members).filter(m => m.role !== PROJECT_MEMBER_ROLE.CUSTOMER),
 
+  /**
+   * Check if project is for SSO users
+   * @param {Object}  project        project
+   * @return {Boolean} is SSO project
+   */
+  isSSO: project => ssoRefCodes.indexOf(_.get(project, 'details.utm.code')) > -1,
+
   /**
   * Set paginated header and respond with data
   * @param {Object} req HTTP request
@@ -634,6 +834,174 @@ _.assignIn(util, {
 
     return Promise.resolve(null);
   },
+
+  /**
+   * Check if user match the permission rule.
+   *
+   * This method uses permission rule defined in `permissionRule`
+   * and checks that the `user` matches it.
+   *
+   * If we define a rule with `projectRoles` list, we also should provide `projectMembers`
+   * - the list of project members.
+   *
+   * @param {Object}        permissionRule               permission rule
+   * @param {Array<String>} permissionRule.projectRoles  the list of project roles of the user
+   * @param {Array<String>} permissionRule.topcoderRoles the list of Topcoder roles of the user
+   * @param {Object}        user                         user for whom we check permissions
+   * @param {Object}        user.roles                   list of user roles
+   * @param {Object}        user.isMachine               `true` - if it's machine, `false` - real user
+   * @param {Object}        user.scopes                  scopes of user token
+   * @param {Array}         projectMembers               (optional) list of project members - required to check `topcoderRoles`
+   *
+   * @returns {Boolean}     true, if has permission
+   */
+  matchPermissionRule: (permissionRule, user, projectMembers) => {
+    const member = _.find(projectMembers, { userId: user.userId });
+    let hasProjectRole = false;
+    let hasTopcoderRole = false;
+
+    if (permissionRule) {
+      if (permissionRule.projectRoles
+        && permissionRule.projectRoles.length > 0
+        && !!member
+      ) {
+        hasProjectRole = _.includes(permissionRule.projectRoles, member.role);
+      }
+
+      if (permissionRule.topcoderRoles && permissionRule.topcoderRoles.length > 0) {
+        hasTopcoderRole = util.hasRoles({ authUser: user }, permissionRule.topcoderRoles);
+      }
+    }
+
+    return hasProjectRole || hasTopcoderRole;
+  },
+
+  /**
+   * Check if user has permission.
+   *
+   * This method uses permission defined in `permission` and checks that the `user` matches it.
+   *
+   * `permission` may be defined in two ways:
+   *  - **Full** way with defined `allowRule` and optional `denyRule`, example:
+   *    ```js
+   *    {
+   *       allowRule: {
+   *          projectRoles: [],
+   *          topcoderRoles: []
+   *       },
+   *       denyRule: {
+   *          projectRoles: [],
+   *          topcoderRoles: []
+   *       }
+   *    }
+   *    ```
+   *    If user matches `denyRule` then the access would be dined even if matches `allowRule`.
+   *  - **Simplified** way may be used if we only want to define `allowRule`.
+   *    We can skip the `allowRule` property and define `allowRule` directly inside `permission` object, example:
+   *    ```js
+   *    {
+   *       projectRoles: [],
+   *       topcoderRoles: []
+   *    }
+   *    ```
+   *    This **simplified** permission is equal to a **full** permission:
+   *    ```js
+   *    {
+   *       allowRule: {
+   *         projectRoles: [],
+   *         topcoderRoles: []
+   *       }
+   *    }
+   *    ```
+   *
+   * If we define any rule with `projectRoles` list, we also should provide `projectMembers`
+   * - the list of project members.
+   *
+   * @param {Object} permission     permission or permissionRule
+   * @param {Object} user           user for whom we check permissions
+   * @param {Object} user.roles     list of user roles
+   * @param {Object} user.isMachine `true` - if it's machine, `false` - real user
+   * @param {Object} user.scopes    scopes of user token
+   * @param {Array}  projectMembers (optional) list of project members - required to check `topcoderRoles`
+   *
+   * @returns {Boolean}     true, if has permission
+   */
+  hasPermission: (permission, user, projectMembers) => {
+    const allowRule = permission.allowRule ? permission.allowRule : permission;
+    const denyRule = permission.denyRule ? permission.denyRule : null;
+
+    const allow = util.matchPermissionRule(allowRule, user, projectMembers);
+    const deny = util.matchPermissionRule(denyRule, user, projectMembers);
+
+    return allow && !deny;
+  },
+
+  /**
+   * Check if user has permission for the project by `projectId`.
+   *
+   * This method uses permission defined in `permission` and checks that the `user` matches it.
+   *
+   * `permission` may be defined in two ways:
+   *  - **Full** way with defined `allowRule` and optional `denyRule`, example:
+   *    ```js
+   *    {
+   *       allowRule: {
+   *          projectRoles: [],
+   *          topcoderRoles: []
+   *       },
+   *       denyRule: {
+   *          projectRoles: [],
+   *          topcoderRoles: []
+   *       }
+   *    }
+   *    ```
+   *    If user matches `denyRule` then the access would be dined even if matches `allowRule`.
+   *  - **Simplified** way may be used if we only want to define `allowRule`.
+   *    We can skip the `allowRule` property and define `allowRule` directly inside `permission` object, example:
+   *    ```js
+   *    {
+   *       projectRoles: [],
+   *       topcoderRoles: []
+   *    }
+   *    ```
+   *    This **simplified** permission is equal to a **full** permission:
+   *    ```js
+   *    {
+   *       allowRule: {
+   *         projectRoles: [],
+   *         topcoderRoles: []
+   *       }
+   *    }
+   *    ```
+   *
+   * @param {Object} permission     permission or permissionRule
+   * @param {Object} user           user for whom we check permissions
+   * @param {Object} user.roles     list of user roles
+   * @param {Object} user.isMachine `true` - if it's machine, `false` - real user
+   * @param {Object} user.scopes    scopes of user token
+   * @param {Number} projectId      project id to check permissions for
+   *
+   * @returns {Promise<Boolean>}     true, if has permission
+   */
+  hasPermissionForProject: (permission, user, projectId) => (
+    models.ProjectMember.getActiveProjectMembers(projectId).then(projectMembers =>
+      util.hasPermission(permission, user, projectMembers),
+    )
+  ),
+
+  /**
+   * Checks if the Project Setting represents price estimation setting
+   *
+   * @param {String} key project setting key
+   *
+   * @returns {Boolean} true it's project setting for price estimation
+   */
+  isProjectSettingForEstimation: (key) => {
+    const markupMatch = key.match(/^markup_(.+)$/);
+    const markupKey = markupMatch && markupMatch[1] ? markupMatch[1] : null;
+
+    return markupKey ? _.includes(_.values(ESTIMATION_TYPE), markupKey) : false;
+  },
 });
 
 export default util;
diff --git a/src/util.spec.js b/src/util.spec.js
new file mode 100644
index 0000000..9972269
--- /dev/null
+++ b/src/util.spec.js
@@ -0,0 +1,39 @@
+/**
+ * Tests for util.js
+ */
+import chai from 'chai';
+import util from './util';
+
+chai.should();
+
+describe('Util method', () => {
+  describe('isProjectSettingForEstimation', () => {
+    it('should return "true" if key is correct: "markup_fee"', () => {
+      util.isProjectSettingForEstimation('markup_fee').should.equal(true);
+    });
+
+    it('should return "false" if key has unknown estimation type: "markup_unknown"', () => {
+      util.isProjectSettingForEstimation('markup_unknown').should.equal(false);
+    });
+
+    it('should return "false" if key doesn\'t have "markup_" prefix: "fee"', () => {
+      util.isProjectSettingForEstimation('fee').should.equal(false);
+    });
+
+    it('should return "false" if key doesn\'t have duplicated prefix "markup_": "markup_markup_fee"', () => {
+      util.isProjectSettingForEstimation('markup_markup_fee').should.equal(false);
+    });
+
+    it('should return "false" if has prefix "markup_" at the end: "feemarkup_"', () => {
+      util.isProjectSettingForEstimation('feemarkup_').should.equal(false);
+    });
+
+    it('should return "false" if has additional text after: "markup_fee_text"', () => {
+      util.isProjectSettingForEstimation('markup_fee_text').should.equal(false);
+    });
+
+    it('should return "false" if has additional text before: "text_markup_fee"', () => {
+      util.isProjectSettingForEstimation('text_markup_fee').should.equal(false);
+    });
+  });
+});