Bug: fix(users): Accepting invite for org_admins



Currently, when email feature flag is enabled and invite a org_admin, it throws error saying `InvalidRoleId`.

When we try to accept invite for `org_admin`s, it again throws `InvalidRoleId`.

This should not be the case, error should not be throws and invite should be accepted.
diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs
index f4142842e21..5ef09715211 100644
--- a/crates/diesel_models/src/query/user_role.rs
+++ b/crates/diesel_models/src/query/user_role.rs
@@ -119,7 +119,7 @@ impl UserRole {
         conn: &PgPooledConn,
         user_id: String,
         org_id: id_type::OrganizationId,
-        merchant_id: id_type::MerchantId,
+        merchant_id: Option<id_type::MerchantId>,
         profile_id: Option<id_type::ProfileId>,
         update: UserRoleUpdate,
         version: UserRoleVersion,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 91c86a74056..046675f5200 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -647,7 +647,14 @@ async fn handle_existing_user_invitation(
     };
 
     let _user_role = match role_info.get_entity_type() {
-        EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Organization => {
+            user_role
+                .add_entity(domain::OrganizationLevel {
+                    org_id: user_from_token.org_id.clone(),
+                })
+                .insert_in_v2(state)
+                .await?
+        }
         EntityType::Merchant => {
             user_role
                 .add_entity(domain::MerchantLevel {
@@ -678,7 +685,10 @@ async fn handle_existing_user_invitation(
     {
         let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
         let entity = match role_info.get_entity_type() {
-            EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Organization => email_types::Entity {
+                entity_id: user_from_token.org_id.get_string_repr().to_owned(),
+                entity_type: EntityType::Organization,
+            },
             EntityType::Merchant => email_types::Entity {
                 entity_id: user_from_token.merchant_id.get_string_repr().to_owned(),
                 entity_type: EntityType::Merchant,
@@ -806,7 +816,10 @@ async fn handle_new_user_invitation(
         let _ = req_state.clone();
         let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
         let entity = match role_info.get_entity_type() {
-            EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Organization => email_types::Entity {
+                entity_id: user_from_token.org_id.get_string_repr().to_owned(),
+                entity_type: EntityType::Organization,
+            },
             EntityType::Merchant => email_types::Entity {
                 entity_id: user_from_token.merchant_id.get_string_repr().to_owned(),
                 entity_type: EntityType::Merchant,
@@ -1012,7 +1025,7 @@ pub async fn accept_invite_from_email_token_only_flow(
         &state,
         user_from_db.get_user_id(),
         &org_id,
-        &merchant_id,
+        merchant_id.as_ref(),
         profile_id.as_ref(),
         UserRoleUpdate::UpdateStatus {
             status: UserStatus::Active,
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index 5973a20dd33..258a88ba9f8 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -166,7 +166,7 @@ pub async fn update_user_role(
             .update_user_role_by_user_id_and_lineage(
                 user_to_be_updated.get_user_id(),
                 &user_from_token.org_id,
-                &user_from_token.merchant_id,
+                Some(&user_from_token.merchant_id),
                 user_from_token.profile_id.as_ref(),
                 UserRoleUpdate::UpdateRole {
                     role_id: req.role_id.clone(),
@@ -231,7 +231,7 @@ pub async fn update_user_role(
             .update_user_role_by_user_id_and_lineage(
                 user_to_be_updated.get_user_id(),
                 &user_from_token.org_id,
-                &user_from_token.merchant_id,
+                Some(&user_from_token.merchant_id),
                 user_from_token.profile_id.as_ref(),
                 UserRoleUpdate::UpdateRole {
                     role_id: req.role_id.clone(),
@@ -280,7 +280,7 @@ pub async fn accept_invitations_v2(
                     &state,
                     user_from_token.user_id.as_str(),
                     org_id,
-                    merchant_id,
+                    merchant_id.as_ref(),
                     profile_id.as_ref(),
                     UserRoleUpdate::UpdateStatus {
                         status: UserStatus::Active,
@@ -332,7 +332,7 @@ pub async fn accept_invitations_pre_auth(
                     &state,
                     user_token.user_id.as_str(),
                     org_id,
-                    merchant_id,
+                    merchant_id.as_ref(),
                     profile_id.as_ref(),
                     UserRoleUpdate::UpdateStatus {
                         status: UserStatus::Active,
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index 33f93ecc699..9f237c226b6 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -3043,7 +3043,7 @@ impl UserRoleInterface for KafkaStore {
         &self,
         user_id: &str,
         org_id: &id_type::OrganizationId,
-        merchant_id: &id_type::MerchantId,
+        merchant_id: Option<&id_type::MerchantId>,
         profile_id: Option<&id_type::ProfileId>,
         update: user_storage::UserRoleUpdate,
         version: enums::UserRoleVersion,
diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs
index d8ef4881934..b589d375975 100644
--- a/crates/router/src/db/user_role.rs
+++ b/crates/router/src/db/user_role.rs
@@ -52,7 +52,7 @@ pub trait UserRoleInterface {
         &self,
         user_id: &str,
         org_id: &id_type::OrganizationId,
-        merchant_id: &id_type::MerchantId,
+        merchant_id: Option<&id_type::MerchantId>,
         profile_id: Option<&id_type::ProfileId>,
         update: storage::UserRoleUpdate,
         version: enums::UserRoleVersion,
@@ -120,7 +120,7 @@ impl UserRoleInterface for Store {
         &self,
         user_id: &str,
         org_id: &id_type::OrganizationId,
-        merchant_id: &id_type::MerchantId,
+        merchant_id: Option<&id_type::MerchantId>,
         profile_id: Option<&id_type::ProfileId>,
         update: storage::UserRoleUpdate,
         version: enums::UserRoleVersion,
@@ -130,7 +130,7 @@ impl UserRoleInterface for Store {
             &conn,
             user_id.to_owned(),
             org_id.to_owned(),
-            merchant_id.to_owned(),
+            merchant_id.cloned(),
             profile_id.cloned(),
             update,
             version,
@@ -280,7 +280,7 @@ impl UserRoleInterface for MockDb {
         &self,
         user_id: &str,
         org_id: &id_type::OrganizationId,
-        merchant_id: &id_type::MerchantId,
+        merchant_id: Option<&id_type::MerchantId>,
         profile_id: Option<&id_type::ProfileId>,
         update: storage::UserRoleUpdate,
         version: enums::UserRoleVersion,
@@ -293,11 +293,11 @@ impl UserRoleInterface for MockDb {
                 && user_role.profile_id.is_none();
 
             let merchant_level_check = user_role.org_id.as_ref() == Some(org_id)
-                && user_role.merchant_id.as_ref() == Some(merchant_id)
+                && user_role.merchant_id.as_ref() == merchant_id
                 && user_role.profile_id.is_none();
 
             let profile_level_check = user_role.org_id.as_ref() == Some(org_id)
-                && user_role.merchant_id.as_ref() == Some(merchant_id)
+                && user_role.merchant_id.as_ref() == merchant_id
                 && user_role.profile_id.as_ref() == profile_id;
 
             // Check if the user role matches the conditions and the version matches
diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs
index b6db8340775..0ea423989f5 100644
--- a/crates/router/src/utils/user_role.rs
+++ b/crates/router/src/utils/user_role.rs
@@ -174,7 +174,7 @@ pub async fn update_v1_and_v2_user_roles_in_db(
     state: &SessionState,
     user_id: &str,
     org_id: &id_type::OrganizationId,
-    merchant_id: &id_type::MerchantId,
+    merchant_id: Option<&id_type::MerchantId>,
     profile_id: Option<&id_type::ProfileId>,
     update: UserRoleUpdate,
 ) -> (
@@ -255,12 +255,57 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite(
 ) -> UserResult<
     Option<(
         id_type::OrganizationId,
-        id_type::MerchantId,
+        Option<id_type::MerchantId>,
         Option<id_type::ProfileId>,
     )>,
 > {
     match entity_type {
-        EntityType::Organization => Err(UserErrors::InvalidRoleOperation.into()),
+        EntityType::Organization => {
+            let Ok(org_id) =
+                id_type::OrganizationId::try_from(std::borrow::Cow::from(entity_id.clone()))
+            else {
+                return Ok(None);
+            };
+
+            let user_roles = state
+                .store
+                .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                    user_id,
+                    org_id: Some(&org_id),
+                    merchant_id: None,
+                    profile_id: None,
+                    entity_id: None,
+                    version: None,
+                    status: Some(UserStatus::InvitationSent),
+                    limit: None,
+                })
+                .await
+                .change_context(UserErrors::InternalServerError)?
+                .into_iter()
+                .collect::<HashSet<_>>();
+
+            if user_roles.len() > 1 {
+                return Ok(None);
+            }
+
+            if let Some(user_role) = user_roles.into_iter().next() {
+                let (_entity_id, entity_type) = user_role
+                    .get_entity_id_and_type()
+                    .ok_or(UserErrors::InternalServerError)?;
+
+                if entity_type != EntityType::Organization {
+                    return Ok(None);
+                }
+
+                return Ok(Some((
+                    user_role.org_id.ok_or(UserErrors::InternalServerError)?,
+                    None,
+                    None,
+                )));
+            }
+
+            Ok(None)
+        }
         EntityType::Merchant => {
             let Ok(merchant_id) = id_type::MerchantId::wrap(entity_id) else {
                 return Ok(None);
@@ -298,7 +343,7 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite(
 
                 return Ok(Some((
                     user_role.org_id.ok_or(UserErrors::InternalServerError)?,
-                    merchant_id,
+                    Some(merchant_id),
                     None,
                 )));
             }
@@ -343,9 +388,11 @@ pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite(
 
                 return Ok(Some((
                     user_role.org_id.ok_or(UserErrors::InternalServerError)?,
-                    user_role
-                        .merchant_id
-                        .ok_or(UserErrors::InternalServerError)?,
+                    Some(
+                        user_role
+                            .merchant_id
+                            .ok_or(UserErrors::InternalServerError)?,
+                    ),
                     Some(profile_id),
                 )));
             }

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

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

## Description
<!-- Describe your changes in detail -->
Currently accepting invites for org_admin will not work. This PR fixes it.

### 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
<!--
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 [#6254](https://github.com/juspay/hyperswitch/issues/6254)

## 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)?
-->
```
curl 'http://localhost:8080/user/accept_invite_from_email' \
  -H 'authorization: Bearer SPT with accept_invite_from_email' \
  --data-raw '{"token":"email token"}'
```
Response:
```json
{
    "token": "JWT",
    "token_type": "user_info"
}
```


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