Bug: feat(users): Add profile level custom role



Currently only merchant level custom role is allowed with role scope as Merchant and organization 

The requirement is to  add  profile level custom role at Organization , Merchant and Profile scope
diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs
index 7c877cd7477..46e15aafc70 100644
--- a/crates/api_models/src/user_role/role.rs
+++ b/crates/api_models/src/user_role/role.rs
@@ -7,6 +7,7 @@ pub struct CreateRoleRequest {
     pub role_name: String,
     pub groups: Vec<PermissionGroup>,
     pub role_scope: RoleScope,
+    pub entity_type: Option<EntityType>,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
@@ -21,6 +22,7 @@ pub struct RoleInfoWithGroupsResponse {
     pub groups: Vec<PermissionGroup>,
     pub role_name: String,
     pub role_scope: RoleScope,
+    pub entity_type: Option<EntityType>,
 }
 
 #[derive(Debug, serde::Serialize)]
diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs
index 917030c1e80..e645b884677 100644
--- a/crates/common_enums/src/enums.rs
+++ b/crates/common_enums/src/enums.rs
@@ -2679,6 +2679,8 @@ pub enum TransactionType {
     Debug,
     Eq,
     PartialEq,
+    Ord,
+    PartialOrd,
     serde::Deserialize,
     serde::Serialize,
     strum::Display,
@@ -2688,8 +2690,19 @@ pub enum TransactionType {
 #[serde(rename_all = "snake_case")]
 #[strum(serialize_all = "snake_case")]
 pub enum RoleScope {
-    Merchant,
-    Organization,
+    Organization = 2,
+    Merchant = 1,
+    Profile = 0,
+}
+
+impl From<RoleScope> for EntityType {
+    fn from(role_scope: RoleScope) -> Self {
+        match role_scope {
+            RoleScope::Organization => Self::Organization,
+            RoleScope::Merchant => Self::Merchant,
+            RoleScope::Profile => Self::Profile,
+        }
+    }
 }
 
 /// Indicates the transaction status
@@ -3132,6 +3145,7 @@ pub enum ApiVersion {
     serde::Serialize,
     strum::Display,
     strum::EnumString,
+    strum::EnumIter,
     ToSchema,
     Hash,
 )]
diff --git a/crates/diesel_models/src/query/role.rs b/crates/diesel_models/src/query/role.rs
index 065a5b6e114..07d2cf2f0a1 100644
--- a/crates/diesel_models/src/query/role.rs
+++ b/crates/diesel_models/src/query/role.rs
@@ -1,4 +1,5 @@
 use async_bb8_diesel::AsyncRunQueryDsl;
+use common_enums::EntityType;
 use common_utils::id_type;
 use diesel::{
     associations::HasTable, debug_query, pg::Pg, result::Error as DieselError,
@@ -106,7 +107,7 @@ impl Role {
         conn: &PgPooledConn,
         org_id: id_type::OrganizationId,
         merchant_id: Option<id_type::MerchantId>,
-        entity_type: Option<common_enums::EntityType>,
+        entity_type: Option<EntityType>,
         limit: Option<u32>,
     ) -> StorageResult<Vec<Self>> {
         let mut query = <Self as HasTable>::table()
@@ -131,6 +132,97 @@ impl Role {
 
         router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
 
+        match generics::db_metrics::track_database_call::<Self, _, _>(
+            query.get_results_async(conn),
+            generics::db_metrics::DatabaseOperation::Filter,
+        )
+        .await
+        {
+            Ok(value) => Ok(value),
+            Err(err) => match err {
+                DieselError::NotFound => {
+                    Err(report!(err)).change_context(errors::DatabaseError::NotFound)
+                }
+                _ => Err(report!(err)).change_context(errors::DatabaseError::Others),
+            },
+        }
+    }
+    pub async fn generic_list_roles_by_entity_type(
+        conn: &PgPooledConn,
+        entity_type: ListRolesByEntityPayload,
+        is_lineage_data_required: bool,
+        limit: Option<u32>,
+    ) -> StorageResult<Vec<Self>> {
+        let mut query = <Self as HasTable>::table().into_boxed();
+
+        match entity_type {
+            ListRolesByEntityPayload::Organization(org_id) => {
+                let entity_in_vec = if is_lineage_data_required {
+                    vec![
+                        EntityType::Organization,
+                        EntityType::Merchant,
+                        EntityType::Profile,
+                    ]
+                } else {
+                    vec![EntityType::Organization]
+                };
+                query = query
+                    .filter(dsl::org_id.eq(org_id))
+                    .filter(
+                        dsl::scope
+                            .eq(RoleScope::Organization)
+                            .or(dsl::scope.eq(RoleScope::Merchant))
+                            .or(dsl::scope.eq(RoleScope::Profile)),
+                    )
+                    .filter(dsl::entity_type.eq_any(entity_in_vec))
+            }
+
+            ListRolesByEntityPayload::Merchant(org_id, merchant_id) => {
+                let entity_in_vec = if is_lineage_data_required {
+                    vec![EntityType::Merchant, EntityType::Profile]
+                } else {
+                    vec![EntityType::Merchant]
+                };
+                query = query
+                    .filter(dsl::org_id.eq(org_id))
+                    .filter(
+                        dsl::scope
+                            .eq(RoleScope::Organization)
+                            .or(dsl::scope
+                                .eq(RoleScope::Merchant)
+                                .and(dsl::merchant_id.eq(merchant_id.clone())))
+                            .or(dsl::scope
+                                .eq(RoleScope::Profile)
+                                .and(dsl::merchant_id.eq(merchant_id))),
+                    )
+                    .filter(dsl::entity_type.eq_any(entity_in_vec))
+            }
+
+            ListRolesByEntityPayload::Profile(org_id, merchant_id, profile_id) => {
+                let entity_in_vec = vec![EntityType::Profile];
+                query = query
+                    .filter(dsl::org_id.eq(org_id))
+                    .filter(
+                        dsl::scope
+                            .eq(RoleScope::Organization)
+                            .or(dsl::scope
+                                .eq(RoleScope::Merchant)
+                                .and(dsl::merchant_id.eq(merchant_id.clone())))
+                            .or(dsl::scope
+                                .eq(RoleScope::Profile)
+                                .and(dsl::merchant_id.eq(merchant_id))
+                                .and(dsl::profile_id.eq(profile_id))),
+                    )
+                    .filter(dsl::entity_type.eq_any(entity_in_vec))
+            }
+        };
+
+        if let Some(limit) = limit {
+            query = query.limit(limit.into());
+        }
+
+        router_env::logger::debug!(query = %debug_query::<Pg,_>(&query).to_string());
+
         match generics::db_metrics::track_database_call::<Self, _, _>(
             query.get_results_async(conn),
             generics::db_metrics::DatabaseOperation::Filter,
diff --git a/crates/diesel_models/src/role.rs b/crates/diesel_models/src/role.rs
index 8199bd3979c..d96421f2db8 100644
--- a/crates/diesel_models/src/role.rs
+++ b/crates/diesel_models/src/role.rs
@@ -19,6 +19,7 @@ pub struct Role {
     pub last_modified_at: PrimitiveDateTime,
     pub last_modified_by: String,
     pub entity_type: enums::EntityType,
+    pub profile_id: Option<id_type::ProfileId>,
 }
 
 #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
@@ -36,6 +37,7 @@ pub struct RoleNew {
     pub last_modified_at: PrimitiveDateTime,
     pub last_modified_by: String,
     pub entity_type: enums::EntityType,
+    pub profile_id: Option<id_type::ProfileId>,
 }
 
 #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)]
@@ -73,3 +75,13 @@ impl From<RoleUpdate> for RoleUpdateInternal {
         }
     }
 }
+
+pub enum ListRolesByEntityPayload {
+    Profile(
+        id_type::OrganizationId,
+        id_type::MerchantId,
+        id_type::ProfileId,
+    ),
+    Merchant(id_type::OrganizationId, id_type::MerchantId),
+    Organization(id_type::OrganizationId),
+}
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index e2ab676b2d3..fd3752cecc9 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1236,6 +1236,8 @@ diesel::table! {
         last_modified_by -> Varchar,
         #[max_length = 64]
         entity_type -> Varchar,
+        #[max_length = 64]
+        profile_id -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 5651bf95dd9..c694751f5bd 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1182,6 +1182,8 @@ diesel::table! {
         last_modified_by -> Varchar,
         #[max_length = 64]
         entity_type -> Varchar,
+        #[max_length = 64]
+        profile_id -> Nullable<Varchar>,
     }
 }
 
diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs
index 0250415d4fd..48f5faccc58 100644
--- a/crates/router/src/core/user_role/role.rs
+++ b/crates/router/src/core/user_role/role.rs
@@ -3,7 +3,7 @@ use std::collections::HashSet;
 use api_models::user_role::role::{self as role_api};
 use common_enums::{EntityType, ParentGroup, PermissionGroup, RoleScope};
 use common_utils::generate_id_with_default_len;
-use diesel_models::role::{RoleNew, RoleUpdate};
+use diesel_models::role::{ListRolesByEntityPayload, RoleNew, RoleUpdate};
 use error_stack::{report, ResultExt};
 
 use crate::{
@@ -65,6 +65,39 @@ pub async fn create_role(
     _req_state: ReqState,
 ) -> UserResponse<role_api::RoleInfoWithGroupsResponse> {
     let now = common_utils::date_time::now();
+
+    let user_entity_type = user_from_token
+        .get_role_info_from_db(&state)
+        .await
+        .attach_printable("Invalid role_id in JWT")?
+        .get_entity_type();
+
+    let role_entity_type = req.entity_type.unwrap_or(EntityType::Merchant);
+
+    if matches!(role_entity_type, EntityType::Organization) {
+        return Err(report!(UserErrors::InvalidRoleOperation))
+            .attach_printable("User trying to create org level custom role");
+    }
+
+    let requestor_entity_from_role_scope = EntityType::from(req.role_scope);
+
+    if !(user_entity_type >= role_entity_type) {
+        return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!(
+            "{} is trying to create {} ",
+            user_entity_type, role_entity_type
+        ));
+    } else if !(user_entity_type >= requestor_entity_from_role_scope) {
+        return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!(
+            "{} is trying to create role of scope {} ",
+            user_entity_type, requestor_entity_from_role_scope
+        ));
+    } else if !(requestor_entity_from_role_scope >= role_entity_type) {
+        return Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!(
+            "User is trying to create role of type {} and scope {}",
+            requestor_entity_from_role_scope, role_entity_type
+        ));
+    }
+
     let role_name = RoleName::new(req.role_name)?;
 
     utils::user_role::validate_role_groups(&req.groups)?;
@@ -76,12 +109,9 @@ pub async fn create_role(
     )
     .await?;
 
-    if matches!(req.role_scope, RoleScope::Organization)
-        && user_from_token.role_id != common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN
-    {
-        return Err(report!(UserErrors::InvalidRoleOperation))
-            .attach_printable("Non org admin user creating org level role");
-    }
+    let profile_id = matches!(role_entity_type, EntityType::Profile)
+        .then_some(user_from_token.profile_id)
+        .flatten();
 
     let role = state
         .store
@@ -92,11 +122,12 @@ pub async fn create_role(
             org_id: user_from_token.org_id,
             groups: req.groups,
             scope: req.role_scope,
-            entity_type: EntityType::Merchant,
+            entity_type: role_entity_type,
             created_by: user_from_token.user_id.clone(),
             last_modified_by: user_from_token.user_id,
             created_at: now,
             last_modified_at: now,
+            profile_id,
         })
         .await
         .to_duplicate_response(UserErrors::RoleNameAlreadyExists)?;
@@ -107,6 +138,7 @@ pub async fn create_role(
             role_id: role.role_id,
             role_name: role.role_name,
             role_scope: role.scope,
+            entity_type: Some(role.entity_type),
         },
     ))
 }
@@ -135,6 +167,7 @@ pub async fn get_role_with_groups(
             role_id: role.role_id,
             role_name: role_info.get_role_name().to_string(),
             role_scope: role_info.get_scope(),
+            entity_type: Some(role_info.get_entity_type()),
         },
     ))
 }
@@ -245,6 +278,7 @@ pub async fn update_role(
             role_id: updated_role.role_id,
             role_name: updated_role.role_name,
             role_scope: updated_role.scope,
+            entity_type: Some(updated_role.entity_type),
         },
     ))
 }
@@ -276,10 +310,9 @@ pub async fn list_roles_with_info(
         match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? {
             EntityType::Organization => state
                 .store
-                .list_roles_for_org_by_parameters(
-                    &user_from_token.org_id,
-                    None,
-                    request.entity_type,
+                .generic_list_roles_by_entity_type(
+                    ListRolesByEntityPayload::Organization(user_from_token.org_id),
+                    request.entity_type.is_none(),
                     None,
                 )
                 .await
@@ -287,17 +320,37 @@ pub async fn list_roles_with_info(
                 .attach_printable("Failed to get roles")?,
             EntityType::Merchant => state
                 .store
-                .list_roles_for_org_by_parameters(
-                    &user_from_token.org_id,
-                    Some(&user_from_token.merchant_id),
-                    request.entity_type,
+                .generic_list_roles_by_entity_type(
+                    ListRolesByEntityPayload::Merchant(
+                        user_from_token.org_id,
+                        user_from_token.merchant_id,
+                    ),
+                    request.entity_type.is_none(),
                     None,
                 )
                 .await
                 .change_context(UserErrors::InternalServerError)
                 .attach_printable("Failed to get roles")?,
-            // TODO: Populate this from Db function when support for profile id and profile level custom roles is added
-            EntityType::Profile => Vec::new(),
+
+            EntityType::Profile => {
+                let Some(profile_id) = user_from_token.profile_id else {
+                    return Err(UserErrors::JwtProfileIdMissing.into());
+                };
+                state
+                    .store
+                    .generic_list_roles_by_entity_type(
+                        ListRolesByEntityPayload::Profile(
+                            user_from_token.org_id,
+                            user_from_token.merchant_id,
+                            profile_id,
+                        ),
+                        request.entity_type.is_none(),
+                        None,
+                    )
+                    .await
+                    .change_context(UserErrors::InternalServerError)
+                    .attach_printable("Failed to get roles")?
+            }
         };
 
     role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from));
@@ -349,10 +402,9 @@ pub async fn list_roles_at_entity_level(
     let custom_roles = match req.entity_type {
         EntityType::Organization => state
             .store
-            .list_roles_for_org_by_parameters(
-                &user_from_token.org_id,
-                None,
-                Some(req.entity_type),
+            .generic_list_roles_by_entity_type(
+                ListRolesByEntityPayload::Organization(user_from_token.org_id),
+                false,
                 None,
             )
             .await
@@ -361,17 +413,38 @@ pub async fn list_roles_at_entity_level(
 
         EntityType::Merchant => state
             .store
-            .list_roles_for_org_by_parameters(
-                &user_from_token.org_id,
-                Some(&user_from_token.merchant_id),
-                Some(req.entity_type),
+            .generic_list_roles_by_entity_type(
+                ListRolesByEntityPayload::Merchant(
+                    user_from_token.org_id,
+                    user_from_token.merchant_id,
+                ),
+                false,
                 None,
             )
             .await
             .change_context(UserErrors::InternalServerError)
             .attach_printable("Failed to get roles")?,
-        // TODO: Populate this from Db function when support for profile id and profile level custom roles is added
-        EntityType::Profile => Vec::new(),
+
+        EntityType::Profile => {
+            let Some(profile_id) = user_from_token.profile_id else {
+                return Err(UserErrors::JwtProfileIdMissing.into());
+            };
+
+            state
+                .store
+                .generic_list_roles_by_entity_type(
+                    ListRolesByEntityPayload::Profile(
+                        user_from_token.org_id,
+                        user_from_token.merchant_id,
+                        profile_id,
+                    ),
+                    false,
+                    None,
+                )
+                .await
+                .change_context(UserErrors::InternalServerError)
+                .attach_printable("Failed to get roles")?
+        }
     };
 
     role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from));
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 8ca0b293766..58c439c7371 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -3534,6 +3534,17 @@ impl RoleInterface for KafkaStore {
             .list_roles_for_org_by_parameters(org_id, merchant_id, entity_type, limit)
             .await
     }
+
+    async fn generic_list_roles_by_entity_type(
+        &self,
+        entity_type: diesel_models::role::ListRolesByEntityPayload,
+        is_lineage_data_required: bool,
+        limit: Option<u32>,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        self.diesel_store
+            .generic_list_roles_by_entity_type(entity_type, is_lineage_data_required, limit)
+            .await
+    }
 }
 
 #[async_trait::async_trait]
diff --git a/crates/router/src/db/role.rs b/crates/router/src/db/role.rs
index d13508356e5..4ec50de023d 100644
--- a/crates/router/src/db/role.rs
+++ b/crates/router/src/db/role.rs
@@ -1,6 +1,8 @@
-use common_enums::enums;
 use common_utils::id_type;
-use diesel_models::role as storage;
+use diesel_models::{
+    enums::{EntityType, RoleScope},
+    role as storage,
+};
 use error_stack::report;
 use router_env::{instrument, tracing};
 
@@ -57,7 +59,14 @@ pub trait RoleInterface {
         &self,
         org_id: &id_type::OrganizationId,
         merchant_id: Option<&id_type::MerchantId>,
-        entity_type: Option<enums::EntityType>,
+        entity_type: Option<EntityType>,
+        limit: Option<u32>,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError>;
+
+    async fn generic_list_roles_by_entity_type(
+        &self,
+        entity_type: storage::ListRolesByEntityPayload,
+        is_lineage_data_required: bool,
         limit: Option<u32>,
     ) -> CustomResult<Vec<storage::Role>, errors::StorageError>;
 }
@@ -151,7 +160,7 @@ impl RoleInterface for Store {
         &self,
         org_id: &id_type::OrganizationId,
         merchant_id: Option<&id_type::MerchantId>,
-        entity_type: Option<enums::EntityType>,
+        entity_type: Option<EntityType>,
         limit: Option<u32>,
     ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
         let conn = connection::pg_connection_read(self).await?;
@@ -165,6 +174,24 @@ impl RoleInterface for Store {
         .await
         .map_err(|error| report!(errors::StorageError::from(error)))
     }
+
+    #[instrument(skip_all)]
+    async fn generic_list_roles_by_entity_type(
+        &self,
+        entity_type: storage::ListRolesByEntityPayload,
+        is_lineage_data_required: bool,
+        limit: Option<u32>,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        let conn = connection::pg_connection_read(self).await?;
+        storage::Role::generic_list_roles_by_entity_type(
+            &conn,
+            entity_type,
+            is_lineage_data_required,
+            limit,
+        )
+        .await
+        .map_err(|error| report!(errors::StorageError::from(error)))
+    }
 }
 
 #[async_trait::async_trait]
@@ -195,6 +222,7 @@ impl RoleInterface for MockDb {
             created_at: role.created_at,
             last_modified_at: role.last_modified_at,
             last_modified_by: role.last_modified_by,
+            profile_id: role.profile_id,
         };
         roles.push(role.clone());
         Ok(role)
@@ -229,7 +257,7 @@ impl RoleInterface for MockDb {
             .find(|role| {
                 role.role_id == role_id
                     && (role.merchant_id == *merchant_id
-                        || (role.org_id == *org_id && role.scope == enums::RoleScope::Organization))
+                        || (role.org_id == *org_id && role.scope == RoleScope::Organization))
             })
             .cloned()
             .ok_or(
@@ -319,8 +347,7 @@ impl RoleInterface for MockDb {
             .iter()
             .filter(|role| {
                 role.merchant_id == *merchant_id
-                    || (role.org_id == *org_id
-                        && role.scope == diesel_models::enums::RoleScope::Organization)
+                    || (role.org_id == *org_id && role.scope == RoleScope::Organization)
             })
             .cloned()
             .collect();
@@ -341,7 +368,7 @@ impl RoleInterface for MockDb {
         &self,
         org_id: &id_type::OrganizationId,
         merchant_id: Option<&id_type::MerchantId>,
-        entity_type: Option<enums::EntityType>,
+        entity_type: Option<EntityType>,
         limit: Option<u32>,
     ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
         let roles = self.roles.lock().await;
@@ -362,4 +389,83 @@ impl RoleInterface for MockDb {
 
         Ok(roles_list)
     }
+
+    #[instrument(skip_all)]
+    async fn generic_list_roles_by_entity_type(
+        &self,
+        entity_type: storage::ListRolesByEntityPayload,
+        is_lineage_data_required: bool,
+        limit: Option<u32>,
+    ) -> CustomResult<Vec<storage::Role>, errors::StorageError> {
+        let roles = self.roles.lock().await;
+        let limit_usize = limit.unwrap_or(u32::MAX).try_into().unwrap_or(usize::MAX);
+        let roles_list: Vec<_> = roles
+            .iter()
+            .filter(|role| match &entity_type {
+                storage::ListRolesByEntityPayload::Organization(org_id) => {
+                    let entity_in_vec = if is_lineage_data_required {
+                        vec![
+                            EntityType::Organization,
+                            EntityType::Merchant,
+                            EntityType::Profile,
+                        ]
+                    } else {
+                        vec![EntityType::Organization]
+                    };
+
+                    let matches_scope = role.scope == RoleScope::Organization
+                        || role.scope == RoleScope::Merchant
+                        || role.scope == RoleScope::Profile;
+
+                    role.org_id == *org_id
+                        && (matches_scope)
+                        && entity_in_vec.contains(&role.entity_type)
+                }
+                storage::ListRolesByEntityPayload::Merchant(org_id, merchant_id) => {
+                    let entity_in_vec = if is_lineage_data_required {
+                        vec![EntityType::Merchant, EntityType::Profile]
+                    } else {
+                        vec![EntityType::Merchant]
+                    };
+
+                    let matches_merchant =
+                        role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant;
+                    let matches_profile =
+                        role.merchant_id == *merchant_id && role.scope == RoleScope::Profile;
+
+                    role.org_id == *org_id
+                        && (role.scope == RoleScope::Organization
+                            || matches_merchant
+                            || matches_profile)
+                        && entity_in_vec.contains(&role.entity_type)
+                }
+                storage::ListRolesByEntityPayload::Profile(org_id, merchant_id, profile_id) => {
+                    let entity_in_vec = [EntityType::Profile];
+
+                    let matches_merchant =
+                        role.merchant_id == *merchant_id && role.scope == RoleScope::Merchant;
+
+                    let matches_profile = role.merchant_id == *merchant_id
+                        && role
+                            .profile_id
+                            .as_ref()
+                            .map(|profile_id_from_role| {
+                                profile_id_from_role == profile_id
+                                    && role.scope == RoleScope::Profile
+                            })
+                            .unwrap_or(true);
+
+                    role.org_id == *org_id
+                        && (role.scope == RoleScope::Organization
+                            || matches_merchant
+                            || matches_profile)
+                        && entity_in_vec.contains(&role.entity_type)
+                }
+            })
+            .take(limit_usize)
+            .cloned()
+            .collect();
+
+        Ok(roles_list)
+    }
 }
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 9910cef3950..79b9be6b803 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -88,7 +88,7 @@ pub async fn create_role(
         json_payload.into_inner(),
         role_core::create_role,
         &auth::JWTAuth {
-            permission: Permission::MerchantUserWrite,
+            permission: Permission::ProfileUserWrite,
         },
         api_locking::LockAction::NotApplicable,
     ))
diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql
new file mode 100644
index 00000000000..d611be2c3da
--- /dev/null
+++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE roles DROP COLUMN IF EXISTS profile_id;
\ No newline at end of file
diff --git a/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql
new file mode 100644
index 00000000000..b3873266fec
--- /dev/null
+++ b/migrations/2024-10-17-073555_add-profile-id-to-roles/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+ALTER TABLE roles ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64);
\ No newline at end of file
diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql
new file mode 100644
index 00000000000..c7c9cbeb401
--- /dev/null
+++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+SELECT 1;
\ No newline at end of file
diff --git a/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql
new file mode 100644
index 00000000000..6fd9b07fd50
--- /dev/null
+++ b/migrations/2024-10-17-123943_add-profile-enum-in-role-scope/up.sql
@@ -0,0 +1,3 @@
+-- Your SQL goes here
+ALTER TYPE "RoleScope"
+ADD VALUE IF NOT EXISTS 'profile';
\ No newline at end of file

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

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

## Description
<!-- Describe your changes in detail -->
Earlier we only had access to create Merchant level custom role at Org and Merchant scope . But after this user can be able to create custom role at Profile level at Organization , Merchant and Profile scope 


### Additional Changes

- [ ] 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
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
Closes [6488](https://github.com/juspay/hyperswitch/issues/6488)

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
**Create Custom role at profile level**
```sh
curl --location 'http://localhost:8080/user/role' \
--header 'authorization: Bearer user JWT Token' \
--data '{
  "role_scope": "organization",
  "groups": [
    "operations_view",
    "operations_manage",
    "connectors_view"
  ],
  "role_name": "org-scope-profile-role-org-user",
  "entity_type":"profile"
}'
```
Response : 
```json
{
    "role_id": "some_role_id",
    "groups": [
        "operations_view",
        "operations_manage",
        "connectors_view"
    ],
    "role_name": "org-scope-profile-role-org-user",
    "role_scope": "organization",
    "entity_type": "profile"
}
```
To create a profile level custom role , the following scenarios the operation should be allowed
| Role scope  | Org level user | merchant level user | Profile level user |
|-------------|----------------|---------------------|--------------------|
| Org         | true           | false               | false              |
| Merchant    | true           | true                | false              |
| Profile     | true           | true                | true               |

**Create Custom role at merchant level**
```sh
curl --location 'http://localhost:8080/user/role' \
--header 'authorization: Bearer user JWT Token' \
--data '{
  "role_scope": "organization",
  "groups": [
    "operations_view",
    "operations_manage",
    "connectors_view"
  ],
  "role_name": "org-scope-merchnant-role-org-user",
  "entity_type":"merchant"
}'
```
Response : 
```json
{
    "role_id": "some_role_id",
    "groups": [
        "operations_view",
        "operations_manage",
        "connectors_view"
    ],
    "role_name": "org-scope-merchnant-role-org-user",
    "role_scope": "organization",
    "entity_type": "merchant"
}
```
To create a merchant level custom role , the following scenarios the operation should be allowed
| Role scope  | Org level user | merchant level user | Profile level user |
|-------------|----------------|---------------------|--------------------|
| Org         | true           | false               | false              |
| Merchant    | true           | true                | false              |
| Profile     | false          | false               | false              |







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

- [ ] I formatted the code `cargo +nightly fmt --all`
- [ ] I addressed lints thrown by `cargo clippy`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
