Bug: create api for custom roles



Create apis for 
- create custom role
- update custom role
diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs
index 2b8d0221497..f19ff95f9ed 100644
--- a/crates/api_models/src/events/user_role.rs
+++ b/crates/api_models/src/events/user_role.rs
@@ -1,8 +1,11 @@
 use common_utils::events::{ApiEventMetric, ApiEventsType};
 
 use crate::user_role::{
-    AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, GetRoleRequest,
-    ListRolesResponse, RoleInfoResponse, TransferOrgOwnershipRequest, UpdateUserRoleRequest,
+    role::{
+        CreateRoleRequest, GetRoleRequest, ListRolesResponse, RoleInfoResponse, UpdateRoleRequest,
+    },
+    AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest,
+    TransferOrgOwnershipRequest, UpdateUserRoleRequest,
 };
 
 common_utils::impl_misc_api_event_type!(
@@ -13,5 +16,7 @@ common_utils::impl_misc_api_event_type!(
     UpdateUserRoleRequest,
     AcceptInvitationRequest,
     DeleteUserRoleRequest,
-    TransferOrgOwnershipRequest
+    TransferOrgOwnershipRequest,
+    CreateRoleRequest,
+    UpdateRoleRequest
 );
diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs
index 1c4c28aa993..ed0838adc24 100644
--- a/crates/api_models/src/user_role.rs
+++ b/crates/api_models/src/user_role.rs
@@ -1,23 +1,8 @@
-use common_enums::RoleScope;
 use common_utils::pii;
 
 use crate::user::DashboardEntryResponse;
 
-#[derive(Debug, serde::Serialize)]
-pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
-
-#[derive(Debug, serde::Serialize)]
-pub struct RoleInfoResponse {
-    pub role_id: String,
-    pub permissions: Vec<Permission>,
-    pub role_name: String,
-    pub role_scope: RoleScope,
-}
-
-#[derive(Debug, serde::Deserialize, serde::Serialize)]
-pub struct GetRoleRequest {
-    pub role_id: String,
-}
+pub mod role;
 
 #[derive(Debug, serde::Serialize)]
 pub enum Permission {
diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs
new file mode 100644
index 00000000000..4a767cfa723
--- /dev/null
+++ b/crates/api_models/src/user_role/role.rs
@@ -0,0 +1,30 @@
+use common_enums::{PermissionGroup, RoleScope};
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct CreateRoleRequest {
+    pub role_name: String,
+    pub groups: Vec<PermissionGroup>,
+    pub role_scope: RoleScope,
+}
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct UpdateRoleRequest {
+    pub groups: Option<Vec<PermissionGroup>>,
+    pub role_name: Option<String>,
+}
+
+#[derive(Debug, serde::Serialize)]
+pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
+
+#[derive(Debug, serde::Serialize)]
+pub struct RoleInfoResponse {
+    pub role_id: String,
+    pub permissions: Vec<super::Permission>,
+    pub role_name: String,
+    pub role_scope: RoleScope,
+}
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct GetRoleRequest {
+    pub role_id: String,
+}
diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs
index 6b21b1461cd..075d0602554 100644
--- a/crates/diesel_models/src/role.rs
+++ b/crates/diesel_models/src/role.rs
@@ -46,35 +46,25 @@ pub struct RoleUpdateInternal {
 }
 
 pub enum RoleUpdate {
-    UpdateGroup {
-        groups: Vec<enums::PermissionGroup>,
-        last_modified_by: String,
-    },
-    UpdateRoleName {
-        role_name: String,
+    UpdateDetails {
+        groups: Option<Vec<enums::PermissionGroup>>,
+        role_name: Option<String>,
+        last_modified_at: PrimitiveDateTime,
         last_modified_by: String,
     },
 }
 
 impl From<RoleUpdate> for RoleUpdateInternal {
     fn from(value: RoleUpdate) -> Self {
-        let last_modified_at = common_utils::date_time::now();
         match value {
-            RoleUpdate::UpdateGroup {
+            RoleUpdate::UpdateDetails {
                 groups,
-                last_modified_by,
-            } => Self {
-                groups: Some(groups),
-                role_name: None,
-                last_modified_at,
-                last_modified_by,
-            },
-            RoleUpdate::UpdateRoleName {
                 role_name,
                 last_modified_by,
+                last_modified_at,
             } => Self {
-                groups: None,
-                role_name: Some(role_name),
+                groups,
+                role_name,
                 last_modified_at,
                 last_modified_by,
             },
diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs
index ae1436bcd67..0a5d6556a11 100644
--- a/crates/router/src/consts/user_role.rs
+++ b/crates/router/src/consts/user_role.rs
@@ -9,3 +9,4 @@ pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer";
 pub const ROLE_ID_MERCHANT_OPERATOR: &str = "merchant_operator";
 pub const ROLE_ID_MERCHANT_CUSTOMER_SUPPORT: &str = "merchant_customer_support";
 pub const INTERNAL_USER_MERCHANT_ID: &str = "juspay000";
+pub const MAX_ROLE_NAME_LENGTH: usize = 64;
diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs
index c837fd7c20f..8a7d77cdbc9 100644
--- a/crates/router/src/core/errors/user.rs
+++ b/crates/router/src/core/errors/user.rs
@@ -62,6 +62,10 @@ pub enum UserErrors {
     RoleNotFound,
     #[error("InvalidRoleOperationWithMessage")]
     InvalidRoleOperationWithMessage(String),
+    #[error("RoleNameParsingError")]
+    RoleNameParsingError,
+    #[error("RoleNameAlreadyExists")]
+    RoleNameAlreadyExists,
 }
 
 impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
@@ -159,6 +163,12 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
             Self::InvalidRoleOperationWithMessage(_) => {
                 AER::BadRequest(ApiError::new(sub_code, 33, self.get_error_message(), None))
             }
+            Self::RoleNameParsingError => {
+                AER::BadRequest(ApiError::new(sub_code, 34, self.get_error_message(), None))
+            }
+            Self::RoleNameAlreadyExists => {
+                AER::BadRequest(ApiError::new(sub_code, 35, self.get_error_message(), None))
+            }
         }
     }
 }
@@ -193,6 +203,8 @@ impl UserErrors {
             Self::MaxInvitationsError => "Maximum invite count per request exceeded",
             Self::RoleNotFound => "Role Not Found",
             Self::InvalidRoleOperationWithMessage(error_message) => error_message,
+            Self::RoleNameParsingError => "Invalid Role Name",
+            Self::RoleNameAlreadyExists => "Role name already exists",
         }
     }
 }
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index c2dfd34322c..b954e0df6c6 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -17,6 +17,8 @@ use crate::{
     utils,
 };
 
+pub mod role;
+
 pub async fn get_authorization_info(
     _state: AppState,
 ) -> UserResponse<user_role_api::AuthorizationInfoResponse> {
@@ -30,98 +32,6 @@ pub async fn get_authorization_info(
     ))
 }
 
-pub async fn list_invitable_roles(
-    state: AppState,
-    user_from_token: auth::UserFromToken,
-) -> UserResponse<user_role_api::ListRolesResponse> {
-    let predefined_roles_map = roles::predefined_roles::PREDEFINED_ROLES
-        .iter()
-        .filter(|(_, role_info)| role_info.is_invitable())
-        .map(|(role_id, role_info)| user_role_api::RoleInfoResponse {
-            permissions: role_info
-                .get_permissions_set()
-                .into_iter()
-                .map(Into::into)
-                .collect(),
-            role_id: role_id.to_string(),
-            role_name: role_info.get_role_name().to_string(),
-            role_scope: role_info.get_scope(),
-        });
-
-    let custom_roles_map = state
-        .store
-        .list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id)
-        .await
-        .change_context(UserErrors::InternalServerError)?
-        .into_iter()
-        .map(roles::RoleInfo::from)
-        .filter(|role_info| role_info.is_invitable())
-        .map(|role_info| user_role_api::RoleInfoResponse {
-            permissions: role_info
-                .get_permissions_set()
-                .into_iter()
-                .map(Into::into)
-                .collect(),
-            role_id: role_info.get_role_id().to_string(),
-            role_name: role_info.get_role_name().to_string(),
-            role_scope: role_info.get_scope(),
-        });
-
-    Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse(
-        predefined_roles_map.chain(custom_roles_map).collect(),
-    )))
-}
-
-pub async fn get_role(
-    state: AppState,
-    user_from_token: auth::UserFromToken,
-    role: user_role_api::GetRoleRequest,
-) -> UserResponse<user_role_api::RoleInfoResponse> {
-    let role_info = roles::get_role_info_from_role_id(
-        &state,
-        &role.role_id,
-        &user_from_token.merchant_id,
-        &user_from_token.org_id,
-    )
-    .await
-    .to_not_found_response(UserErrors::InvalidRoleId)?;
-
-    if role_info.is_internal() {
-        return Err(UserErrors::InvalidRoleId.into());
-    }
-
-    let permissions = role_info
-        .get_permissions_set()
-        .into_iter()
-        .map(Into::into)
-        .collect();
-
-    Ok(ApplicationResponse::Json(user_role_api::RoleInfoResponse {
-        permissions,
-        role_id: role.role_id,
-        role_name: role_info.get_role_name().to_string(),
-        role_scope: role_info.get_scope(),
-    }))
-}
-
-pub async fn get_role_from_token(
-    state: AppState,
-    user_from_token: auth::UserFromToken,
-) -> UserResponse<Vec<user_role_api::Permission>> {
-    let role_info = user_from_token
-        .get_role_info_from_db(&state)
-        .await
-        .attach_printable("Invalid role_id in JWT")?;
-
-    let permissions = role_info
-        .get_permissions_set()
-        .into_iter()
-        .map(Into::into)
-        .collect();
-
-    Ok(ApplicationResponse::Json(permissions))
-}
-
 pub async fn update_user_role(
     state: AppState,
     user_from_token: auth::UserFromToken,
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
new file mode 100644
index 00000000000..7ce72779bbb
--- /dev/null
+++ b/crates/router/src/core/user_role/role.rs
@@ -0,0 +1,223 @@
+use api_models::user_role::{
+    role::{self as role_api},
+    Permission,
+};
+use common_enums::RoleScope;
+use common_utils::generate_id_with_default_len;
+use diesel_models::role::{RoleNew, RoleUpdate};
+use error_stack::ResultExt;
+
+use crate::{
+    consts,
+    core::errors::{StorageErrorExt, UserErrors, UserResponse},
+    routes::AppState,
+    services::{
+        authentication::UserFromToken,
+        authorization::roles::{self, predefined_roles::PREDEFINED_ROLES},
+        ApplicationResponse,
+    },
+    types::domain::user::RoleName,
+    utils,
+};
+
+pub async fn get_role_from_token(
+    state: AppState,
+    user_from_token: UserFromToken,
+) -> UserResponse<Vec<Permission>> {
+    let role_info = user_from_token
+        .get_role_info_from_db(&state)
+        .await
+        .attach_printable("Invalid role_id in JWT")?;
+
+    let permissions = role_info
+        .get_permissions_set()
+        .into_iter()
+        .map(Into::into)
+        .collect();
+
+    Ok(ApplicationResponse::Json(permissions))
+}
+
+pub async fn create_role(
+    state: AppState,
+    user_from_token: UserFromToken,
+    req: role_api::CreateRoleRequest,
+) -> UserResponse<()> {
+    let now = common_utils::date_time::now();
+    let role_name = RoleName::new(req.role_name)?.get_role_name();
+
+    if req.groups.is_empty() {
+        return Err(UserErrors::InvalidRoleOperation.into())
+            .attach_printable("Role groups cannot be empty");
+    }
+
+    if matches!(req.role_scope, RoleScope::Organization)
+        && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN
+    {
+        return Err(UserErrors::InvalidRoleOperation.into())
+            .attach_printable("Non org admin user creating org level role");
+    }
+
+    utils::user_role::is_role_name_already_present_for_merchant(
+        &state,
+        &role_name,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await?;
+
+    state
+        .store
+        .insert_role(RoleNew {
+            role_id: generate_id_with_default_len("role"),
+            role_name,
+            merchant_id: user_from_token.merchant_id,
+            org_id: user_from_token.org_id,
+            groups: req.groups,
+            scope: req.role_scope,
+            created_by: user_from_token.user_id.clone(),
+            last_modified_by: user_from_token.user_id,
+            created_at: now,
+            last_modified_at: now,
+        })
+        .await
+        .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?;
+
+    Ok(ApplicationResponse::StatusOk)
+}
+
+pub async fn list_invitable_roles(
+    state: AppState,
+    user_from_token: UserFromToken,
+) -> UserResponse<role_api::ListRolesResponse> {
+    let predefined_roles_map = PREDEFINED_ROLES
+        .iter()
+        .filter(|(_, role_info)| role_info.is_invitable())
+        .map(|(role_id, role_info)| role_api::RoleInfoResponse {
+            permissions: role_info
+                .get_permissions_set()
+                .into_iter()
+                .map(Into::into)
+                .collect(),
+            role_id: role_id.to_string(),
+            role_name: role_info.get_role_name().to_string(),
+            role_scope: role_info.get_scope(),
+        });
+
+    let custom_roles_map = state
+        .store
+        .list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into_iter()
+        .map(roles::RoleInfo::from)
+        .filter(|role_info| role_info.is_invitable())
+        .map(|role_info| role_api::RoleInfoResponse {
+            permissions: role_info
+                .get_permissions_set()
+                .into_iter()
+                .map(Into::into)
+                .collect(),
+            role_id: role_info.get_role_id().to_string(),
+            role_name: role_info.get_role_name().to_string(),
+            role_scope: role_info.get_scope(),
+        });
+
+    Ok(ApplicationResponse::Json(role_api::ListRolesResponse(
+        predefined_roles_map.chain(custom_roles_map).collect(),
+    )))
+}
+
+pub async fn get_role(
+    state: AppState,
+    user_from_token: UserFromToken,
+    role: role_api::GetRoleRequest,
+) -> UserResponse<role_api::RoleInfoResponse> {
+    let role_info = roles::get_role_info_from_role_id(
+        &state,
+        &role.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .to_not_found_response(UserErrors::InvalidRoleId)?;
+
+    if role_info.is_internal() {
+        return Err(UserErrors::InvalidRoleId.into());
+    }
+
+    let permissions = role_info
+        .get_permissions_set()
+        .into_iter()
+        .map(Into::into)
+        .collect();
+
+    Ok(ApplicationResponse::Json(role_api::RoleInfoResponse {
+        permissions,
+        role_id: role.role_id,
+        role_name: role_info.get_role_name().to_string(),
+        role_scope: role_info.get_scope(),
+    }))
+}
+
+pub async fn update_role(
+    state: AppState,
+    user_from_token: UserFromToken,
+    req: role_api::UpdateRoleRequest,
+    role_id: &str,
+) -> UserResponse<()> {
+    let role_name = req
+        .role_name
+        .map(RoleName::new)
+        .transpose()?
+        .map(RoleName::get_role_name);
+
+    if let Some(ref role_name) = role_name {
+        utils::user_role::is_role_name_already_present_for_merchant(
+            &state,
+            role_name,
+            &user_from_token.merchant_id,
+            &user_from_token.org_id,
+        )
+        .await?;
+    }
+
+    let role_info = roles::get_role_info_from_role_id(
+        &state,
+        role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .to_not_found_response(UserErrors::InvalidRoleOperation)?;
+
+    if matches!(role_info.get_scope(), RoleScope::Organization)
+        && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN
+    {
+        return Err(UserErrors::InvalidRoleOperation.into())
+            .attach_printable("Non org admin user creating org level role");
+    }
+
+    if let Some(ref groups) = req.groups {
+        if groups.is_empty() {
+            return Err(UserErrors::InvalidRoleOperation.into())
+                .attach_printable("role groups cannot be empty");
+        }
+    }
+
+    state
+        .store
+        .update_role_by_role_id(
+            role_id,
+            RoleUpdate::UpdateDetails {
+                groups: req.groups,
+                role_name,
+                last_modified_at: common_utils::date_time::now(),
+                last_modified_by: user_from_token.user_id,
+            },
+        )
+        .await
+        .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?;
+
+    Ok(ApplicationResponse::StatusOk)
+}
diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs
index 90e1e97e377..111b531e2f4 100644
--- a/crates/router/src/db/role.rs
+++ b/crates/router/src/db/role.rs
@@ -200,29 +200,21 @@ impl RoleInterface for MockDb {
         role_update: storage::RoleUpdate,
     ) -> CustomResult<storage::Role, errors::StorageError> {
         let mut roles = self.roles.lock().await;
-        let last_modified_at = common_utils::date_time::now();
-
         roles
             .iter_mut()
             .find(|role| role.role_id == role_id)
             .map(|role| {
                 *role = match role_update {
-                    storage::RoleUpdate::UpdateGroup {
-                        groups,
-                        last_modified_by,
-                    } => storage::Role {
+                    storage::RoleUpdate::UpdateDetails {
                         groups,
-                        last_modified_by,
-                        last_modified_at,
-                        ..role.to_owned()
-                    },
-                    storage::RoleUpdate::UpdateRoleName {
                         role_name,
+                        last_modified_at,
                         last_modified_by,
                     } => storage::Role {
-                        role_name,
-                        last_modified_at,
+                        groups: groups.unwrap_or(role.groups.to_owned()),
+                        role_name: role_name.unwrap_or(role.role_name.to_owned()),
                         last_modified_by,
+                        last_modified_at,
                         ..role.to_owned()
                     },
                 };
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 26f5b528072..a11265fde80 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1052,9 +1052,17 @@ impl User {
         // Role information
         route = route.service(
             web::scope("/role")
-                .service(web::resource("").route(web::get().to(get_role_from_token)))
+                .service(
+                    web::resource("")
+                        .route(web::get().to(get_role_from_token))
+                        .route(web::post().to(create_role)),
+                )
                 .service(web::resource("/list").route(web::get().to(list_all_roles)))
-                .service(web::resource("/{role_id}").route(web::get().to(get_role))),
+                .service(
+                    web::resource("/{role_id}")
+                        .route(web::get().to(get_role))
+                        .route(web::put().to(update_role)),
+                ),
         );
 
         #[cfg(feature = "dummy_connector")]
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index e3b04bd3f73..0214d3b5e3d 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -29,6 +29,7 @@ pub enum ApiIdentifier {
     Forex,
     RustLockerMigration,
     Gsm,
+    Role,
     User,
     UserRole,
     ConnectorOnboarding,
@@ -200,7 +201,9 @@ impl From<Flow> for ApiIdentifier {
             | Flow::GetAuthorizationInfo
             | Flow::AcceptInvitation
             | Flow::DeleteUserRole
-            | Flow::TransferOrgOwnership => Self::UserRole,
+            | Flow::TransferOrgOwnership
+            | Flow::CreateRole
+            | Flow::UpdateRole => Self::UserRole,
 
             Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => {
                 Self::ConnectorOnboarding
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 63e2ce37269..52e739c36e1 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -7,7 +7,7 @@ use crate::{
     core::{api_locking, user_role as user_role_core},
     services::{
         api,
-        authentication::{self as auth, UserFromToken},
+        authentication::{self as auth},
         authorization::permissions::Permission,
     },
 };
@@ -29,6 +29,38 @@ pub async fn get_authorization_info(
     .await
 }
 
+pub async fn get_role_from_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
+    let flow = Flow::GetRoleFromToken;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        (),
+        |state, user, _| user_role_core::role::get_role_from_token(state, user),
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+pub async fn create_role(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_role_api::role::CreateRoleRequest>,
+) -> HttpResponse {
+    let flow = Flow::CreateRole;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        json_payload.into_inner(),
+        user_role_core::role::create_role,
+        &auth::JWTAuth(Permission::UsersWrite),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
     let flow = Flow::ListRoles;
     Box::pin(api::server_wrap(
@@ -36,7 +68,7 @@ pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> Htt
         state.clone(),
         &req,
         (),
-        |state, user, _| user_role_core::list_invitable_roles(state, user),
+        |state, user, _| user_role_core::role::list_invitable_roles(state, user),
         &auth::JWTAuth(Permission::UsersRead),
         api_locking::LockAction::NotApplicable,
     ))
@@ -49,7 +81,7 @@ pub async fn get_role(
     path: web::Path<String>,
 ) -> HttpResponse {
     let flow = Flow::GetRole;
-    let request_payload = user_role_api::GetRoleRequest {
+    let request_payload = user_role_api::role::GetRoleRequest {
         role_id: path.into_inner(),
     };
     Box::pin(api::server_wrap(
@@ -57,22 +89,29 @@ pub async fn get_role(
         state.clone(),
         &req,
         request_payload,
-        user_role_core::get_role,
+        user_role_core::role::get_role,
         &auth::JWTAuth(Permission::UsersRead),
         api_locking::LockAction::NotApplicable,
     ))
     .await
 }
 
-pub async fn get_role_from_token(state: web::Data<AppState>, req: HttpRequest) -> HttpResponse {
-    let flow = Flow::GetRoleFromToken;
+pub async fn update_role(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_role_api::role::UpdateRoleRequest>,
+    path: web::Path<String>,
+) -> HttpResponse {
+    let flow = Flow::UpdateRole;
+    let role_id = path.into_inner();
+
     Box::pin(api::server_wrap(
         flow,
         state.clone(),
         &req,
-        (),
-        |state, user: UserFromToken, _| user_role_core::get_role_from_token(state, user),
-        &auth::DashboardNoPermissionAuth,
+        json_payload.into_inner(),
+        |state, user, req| user_role_core::role::update_role(state, user, req, &role_id),
+        &auth::JWTAuth(Permission::UsersWrite),
         api_locking::LockAction::NotApplicable,
     ))
     .await
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 263b0e52b8a..a759bf00806 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -926,3 +926,23 @@ impl ForeignFrom<UserStatus> for user_role_api::UserStatus {
         }
     }
 }
+
+#[derive(Clone)]
+pub struct RoleName(String);
+
+impl RoleName {
+    pub fn new(name: String) -> UserResult<Self> {
+        let is_empty_or_whitespace = name.trim().is_empty();
+        let is_too_long = name.graphemes(true).count() > consts::user_role::MAX_ROLE_NAME_LENGTH;
+
+        if is_empty_or_whitespace || is_too_long || name.contains(' ') {
+            Err(UserErrors::RoleNameParsingError.into())
+        } else {
+            Ok(Self(name.to_lowercase()))
+        }
+    }
+
+    pub fn get_role_name(self) -> String {
+        self.0
+    }
+}
diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs
index ef69219b4c9..8ae65ce86e0 100644
--- a/crates/router/src/utils/user_role.rs
+++ b/crates/router/src/utils/user_role.rs
@@ -1,6 +1,11 @@
 use api_models::user_role as user_role_api;
+use error_stack::ResultExt;
 
-use crate::services::authorization::permissions::Permission;
+use crate::{
+    core::errors::{UserErrors, UserResult},
+    routes::AppState,
+    services::authorization::permissions::Permission,
+};
 
 impl From<Permission> for user_role_api::Permission {
     fn from(value: Permission) -> Self {
@@ -34,3 +39,24 @@ impl From<Permission> for user_role_api::Permission {
         }
     }
 }
+
+pub async fn is_role_name_already_present_for_merchant(
+    state: &AppState,
+    role_name: &str,
+    merchant_id: &str,
+    org_id: &str,
+) -> UserResult<()> {
+    let role_name_list: Vec<String> = state
+        .store
+        .list_all_roles(merchant_id, org_id)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .iter()
+        .map(|role| role.role_name.to_owned())
+        .collect();
+
+    if role_name_list.contains(&role_name.to_string()) {
+        return Err(UserErrors::RoleNameAlreadyExists.into());
+    }
+    Ok(())
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index b2e665059e7..4af81ae49ca 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -363,6 +363,10 @@ pub enum Flow {
     UpdateUserAccountDetails,
     /// Accept user invitation
     AcceptInvitation,
+    /// Create Role
+    CreateRole,
+    /// Update Role
+    UpdateRole,
 }
 
 ///
diff --git a/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql b/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql
new file mode 100644
index 00000000000..3fcac875de6
--- /dev/null
+++ b/migrations/2024-02-22-100718_role_name_org_id_constraint/down.sql
@@ -0,0 +1,3 @@
+-- This file should undo anything in `up.sql`
+DROP INDEX role_name_org_id_org_scope_index;
+DROP INDEX role_name_merchant_id_merchant_scope_index;
diff --git a/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql b/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql
new file mode 100644
index 00000000000..9780e01b772
--- /dev/null
+++ b/migrations/2024-02-22-100718_role_name_org_id_constraint/up.sql
@@ -0,0 +1,3 @@
+-- Your SQL goes here
+CREATE UNIQUE INDEX role_name_org_id_org_scope_index ON roles(org_id, role_name) WHERE scope='organization';
+CREATE UNIQUE INDEX role_name_merchant_id_merchant_scope_index ON roles(merchant_id, role_name) WHERE scope='merchant';

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
Add functionality for
- create role api
- update role api

### Additional Changes

- [x] This PR modifies the API contract
- [x] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
User should be allowed to create/update custom roles.


## How did you test it?
Create role
```
curl --location --request PUT 'http://localhost:8080/user/role' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOGJiZGM4YWUtZDlhYS00NGUxLWFkYzYtYjEyMjZiYjkxZDliIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzA4NjAxMTE3Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTcwODc3MzkxNywib3JnX2lkIjoib3JnX1dFN1Z3emNITWRaS01hTzYzeW9pIn0.l5tcWpOqgTVxGKw6zQvx-4iBBmOietj1-drGgzqFYBg' \
--header 'Content-Type: application/json' \
--data '{
  "role_name": "test_x",
  "groups": ["operations_view"],
  "role_scope": "organization"
}'
```
Update
```
curl --location --request PUT 'http://localhost:8080/user/role/role_cau2w3YVoakCr30XWeJi' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOGJiZGM4YWUtZDlhYS00NGUxLWFkYzYtYjEyMjZiYjkxZDliIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzA4NjAxMTE3Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTcwODc3MzkxNywib3JnX2lkIjoib3JnX1dFN1Z3emNITWRaS01hTzYzeW9pIn0.l5tcWpOqgTVxGKw6zQvx-4iBBmOietj1-drGgzqFYBg' \
--header 'Content-Type: application/json' \
--data '{
  "role_name": "test",
  "groups": ["workflows_manage"]
}'
```

Response will be 200 Ok.


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
- [ ] I added a [CHANGELOG](/CHANGELOG.md) entry if applicable
