Bug: feat(user_role): add support to get user role details v2



Add support to return user role details v2
Request will take email and response will return something like
```
[
		{
			role_id: "r1",
			role_name: "name",
			org: {
				name: "Juspay",
				id: "o1"
			},
			merchant: {                       // Optional
				name: "Juspay merchant",
				id: "m1"
			},
			profile: {                        // Optional
				name: "Juspay profile",
				id: "p1"
			},
			status: "status",
			entity_type: "type"
		}
	]
```
diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index 8d1ae60b325..9b100a7321e 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -14,11 +14,11 @@ use crate::user::{
     ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
     CreateUserAuthenticationMethodRequest, DashboardEntryResponse, ForgotPasswordRequest,
     GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse,
-    GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, InviteUserRequest, ListUsersResponse,
-    ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest,
-    SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest,
-    SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse,
-    TwoFactorAuthStatusResponse, UpdateUserAccountDetailsRequest,
+    GetUserRoleDetailsRequest, GetUserRoleDetailsResponse, GetUserRoleDetailsResponseV2,
+    InviteUserRequest, ListUsersResponse, ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest,
+    RotatePasswordRequest, SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest,
+    SsoSignInRequest, SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest,
+    TokenResponse, TwoFactorAuthStatusResponse, UpdateUserAccountDetailsRequest,
     UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantCreate,
     VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest,
 };
@@ -70,6 +70,7 @@ common_utils::impl_api_event_type!(
         GetUserDetailsResponse,
         GetUserRoleDetailsRequest,
         GetUserRoleDetailsResponse,
+        GetUserRoleDetailsResponseV2,
         TokenResponse,
         TwoFactorAuthStatusResponse,
         UserFromEmailRequest,
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index 78157f62bc5..4674abf3976 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -1,3 +1,5 @@
+use std::fmt::Debug;
+
 use common_enums::{PermissionGroup, RoleScope, TokenPurpose};
 use common_utils::{crypto::OptionalEncryptableName, id_type, pii};
 use masking::Secret;
@@ -176,6 +178,22 @@ pub struct GetUserRoleDetailsResponse {
     pub role_scope: RoleScope,
 }
 
+#[derive(Debug, serde::Serialize)]
+pub struct GetUserRoleDetailsResponseV2 {
+    pub role_id: String,
+    pub org: NameIdUnit<Option<String>, id_type::OrganizationId>,
+    pub merchant: Option<NameIdUnit<OptionalEncryptableName, id_type::MerchantId>>,
+    pub profile: Option<NameIdUnit<String, id_type::ProfileId>>,
+    pub status: UserStatus,
+    pub entity_type: common_enums::EntityType,
+}
+
+#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
+pub struct NameIdUnit<N: Debug + Clone, I: Debug + Clone> {
+    pub name: N,
+    pub id: I,
+}
+
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct VerifyEmailRequest {
     pub token: Secret<String>,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 0c4afbe4ecd..1d235dceaea 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -5,7 +5,7 @@ use std::{
 
 use api_models::{
     payments::RedirectionResponse,
-    user::{self as user_api, InviteMultipleUserResponse},
+    user::{self as user_api, InviteMultipleUserResponse, NameIdUnit},
 };
 use common_enums::EntityType;
 use common_utils::{type_name, types::keymanager::Identifier};
@@ -1407,6 +1407,237 @@ pub async fn get_user_details_in_merchant_account(
     ))
 }
 
+pub async fn list_user_roles_details(
+    state: SessionState,
+    user_from_token: auth::UserFromToken,
+    request: user_api::GetUserRoleDetailsRequest,
+    _req_state: ReqState,
+) -> UserResponse<Vec<user_api::GetUserRoleDetailsResponseV2>> {
+    let required_user = utils::user::get_user_from_db_by_email(&state, request.email.try_into()?)
+        .await
+        .to_not_found_response(UserErrors::InvalidRoleOperation)?;
+
+    let requestor_role_info = roles::RoleInfo::from_role_id(
+        &state,
+        &user_from_token.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .to_not_found_response(UserErrors::InternalServerError)
+    .attach_printable("Failed to fetch role info")?;
+
+    let user_roles_set: HashSet<_> = state
+        .store
+        .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+            user_id: required_user.get_user_id(),
+            org_id: Some(&user_from_token.org_id),
+            merchant_id: (requestor_role_info.get_entity_type() <= EntityType::Merchant)
+                .then_some(&user_from_token.merchant_id),
+            profile_id: (requestor_role_info.get_entity_type() <= EntityType::Profile)
+                .then_some(&user_from_token.profile_id)
+                .cloned()
+                .flatten()
+                .as_ref(),
+            entity_id: None,
+            version: None,
+            status: None,
+            limit: None,
+        })
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to fetch user roles")?
+        .into_iter()
+        .collect();
+
+    let org_name = state
+        .store
+        .find_organization_by_org_id(&user_from_token.org_id)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Org id not found")?
+        .get_organization_name();
+
+    let org = NameIdUnit {
+        id: user_from_token.org_id.clone(),
+        name: org_name,
+    };
+
+    let (merchant_ids, merchant_profile_ids) = user_roles_set.iter().try_fold(
+        (Vec::new(), Vec::new()),
+        |(mut merchant, mut merchant_profile), user_role| {
+            let entity_type = user_role
+                .get_entity_id_and_type()
+                .ok_or(UserErrors::InternalServerError)
+                .attach_printable("Failed to fetch entity id and type")?
+                .1;
+            match entity_type {
+                EntityType::Merchant => {
+                    let merchant_id = user_role
+                        .merchant_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)
+                        .attach_printable(
+                            "Merchant id not found in user role for merchant level entity",
+                        )?;
+                    merchant.push(merchant_id)
+                }
+                EntityType::Profile => {
+                    let merchant_id = user_role
+                        .merchant_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)
+                        .attach_printable(
+                            "Merchant id not found in user role for merchant level entity",
+                        )?;
+                    let profile_id = user_role
+                        .profile_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)
+                        .attach_printable(
+                            "Profile id not found in user role for profile level entity",
+                        )?;
+
+                    merchant.push(merchant_id.clone());
+                    merchant_profile.push((merchant_id, profile_id))
+                }
+                EntityType::Internal => {
+                    return Err(UserErrors::InvalidRoleOperationWithMessage(
+                        "Internal roles are not allowed for this operation".to_string(),
+                    )
+                    .into());
+                }
+                EntityType::Organization => (),
+            };
+
+            Ok::<_, error_stack::Report<UserErrors>>((merchant, merchant_profile))
+        },
+    )?;
+
+    let merchant_map: HashMap<_, _> = state
+        .store
+        .list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Error while listing merchant accounts")?
+        .into_iter()
+        .map(|merchant_account| {
+            (
+                merchant_account.get_id().to_owned(),
+                merchant_account.merchant_name.clone(),
+            )
+        })
+        .collect();
+
+    let key_manager_state = &(&state).into();
+
+    let profile_map: HashMap<_, _> =
+        futures::future::try_join_all(merchant_profile_ids.iter().map(
+            |merchant_profile_id| async {
+                let merchant_key_store = state
+                    .store
+                    .get_merchant_key_store_by_merchant_id(
+                        key_manager_state,
+                        &merchant_profile_id.0,
+                        &state.store.get_master_key().to_vec().into(),
+                    )
+                    .await
+                    .change_context(UserErrors::InternalServerError)
+                    .attach_printable("Failed to retrieve merchant key store by merchant_id")?;
+
+                state
+                    .store
+                    .find_business_profile_by_profile_id(
+                        key_manager_state,
+                        &merchant_key_store,
+                        &merchant_profile_id.1,
+                    )
+                    .await
+                    .change_context(UserErrors::InternalServerError)
+                    .attach_printable("Failed to retrieve business profile")
+            },
+        ))
+        .await
+        .change_context(UserErrors::InternalServerError)
+        .attach_printable("Failed to construct proifle map")?
+        .into_iter()
+        .map(|profile| (profile.get_id().to_owned(), profile.profile_name))
+        .collect();
+
+    let role_details_list: Vec<_> = user_roles_set
+        .iter()
+        .map(|user_role| {
+            let entity_type = user_role
+                .get_entity_id_and_type()
+                .ok_or(UserErrors::InternalServerError)?
+                .1;
+            let (merchant, profile) = match entity_type {
+                EntityType::Internal => {
+                    return Err(UserErrors::InvalidRoleOperationWithMessage(
+                        "Internal roles are not allowed for this operation".to_string(),
+                    ));
+                }
+                EntityType::Organization => (None, None),
+                EntityType::Merchant => {
+                    let merchant_id = &user_role
+                        .merchant_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)?;
+
+                    (
+                        Some(NameIdUnit {
+                            id: merchant_id.clone(),
+                            name: merchant_map
+                                .get(merchant_id)
+                                .ok_or(UserErrors::InternalServerError)?
+                                .to_owned(),
+                        }),
+                        None,
+                    )
+                }
+                EntityType::Profile => {
+                    let merchant_id = &user_role
+                        .merchant_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)?;
+                    let profile_id = &user_role
+                        .profile_id
+                        .clone()
+                        .ok_or(UserErrors::InternalServerError)?;
+
+                    (
+                        Some(NameIdUnit {
+                            id: merchant_id.clone(),
+                            name: merchant_map
+                                .get(merchant_id)
+                                .ok_or(UserErrors::InternalServerError)?
+                                .to_owned(),
+                        }),
+                        Some(NameIdUnit {
+                            id: profile_id.clone(),
+                            name: profile_map
+                                .get(profile_id)
+                                .ok_or(UserErrors::InternalServerError)?
+                                .to_owned(),
+                        }),
+                    )
+                }
+            };
+
+            Ok(user_api::GetUserRoleDetailsResponseV2 {
+                role_id: user_role.role_id.clone(),
+                org: org.clone(),
+                merchant,
+                profile,
+                status: user_role.status.foreign_into(),
+                entity_type,
+            })
+        })
+        .collect::<Result<Vec<_>, UserErrors>>()?;
+
+    Ok(ApplicationResponse::Json(role_details_list))
+}
+
 pub async fn list_users_for_merchant_account(
     state: SessionState,
     user_from_token: auth::UserFromToken,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 98ced576105..f57ca47612b 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1817,6 +1817,7 @@ impl User {
         route = route.service(
             web::scope("/user")
                 .service(web::resource("").route(web::get().to(get_user_role_details)))
+                .service(web::resource("/v2").route(web::post().to(list_user_roles_details)))
                 .service(
                     web::resource("/list").route(web::get().to(list_users_for_merchant_account)),
                 )
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 1d5e3d601b6..dfc25c68945 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -336,6 +336,24 @@ pub async fn get_user_role_details(
     .await
 }
 
+pub async fn list_user_roles_details(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Json<user_api::GetUserRoleDetailsRequest>,
+) -> HttpResponse {
+    let flow = Flow::GetUserRoleDetails;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload.into_inner(),
+        user_core::list_user_roles_details,
+        &auth::JWTAuth(Permission::UsersRead),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub async fn list_users_for_merchant_account(
     state: web::Data<AppState>,
     req: HttpRequest,

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

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

## Description
Add support to get user role details

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] 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
Closes [#5776](https://github.com/juspay/hyperswitch/issues/5776)


## How did you test it?
Request:
```
curl --location 'http://localhost:8080/user/user/v2' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer JWT' \
--data-raw '{
    "email": "some_email"
}'
```
Response
```
[
    {
        "role_id": "org_admin",
        "org": {
            "name": null,
            "id": "org_SDzPXs082bpEQt3jbPlE"
        },
        "merchant": null,
        "profile": null,
        "status": "Active",
        "entity_type": "organization"
    }
]
```


## 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
