Bug: feat(users): Add profile level invites



Currently invites are at merchant level only. Invite, accept invite and accept invite from email should change because of this.
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index a0dadfea217..78157f62bc5 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -51,9 +51,6 @@ pub struct AuthorizeResponse {
     //this field is added for audit/debug reasons
     #[serde(skip_serializing)]
     pub user_id: String,
-    //this field is added for audit/debug reasons
-    #[serde(skip_serializing)]
-    pub merchant_id: id_type::MerchantId,
 }
 
 #[derive(serde::Deserialize, Debug, serde::Serialize)]
@@ -209,7 +206,6 @@ pub struct VerifyTokenResponse {
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct UpdateUserAccountDetailsRequest {
     pub name: Option<Secret<String>>,
-    pub preferred_merchant_id: Option<id_type::MerchantId>,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs
index e4819b74fbc..05087d09e80 100644
--- a/crates/api_models/src/user_role.rs
+++ b/crates/api_models/src/user_role.rs
@@ -147,3 +147,12 @@ pub struct ListInvitationForUserResponse {
     pub entity_name: Option<Secret<String>>,
     pub role_id: String,
 }
+
+pub type AcceptInvitationsV2Request = Vec<Entity>;
+pub type AcceptInvitationsPreAuthRequest = Vec<Entity>;
+
+#[derive(Debug, serde::Deserialize, serde::Serialize)]
+pub struct Entity {
+    pub entity_id: String,
+    pub entity_type: common_enums::EntityType,
+}
diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs
index e0f2b9a0bd2..5986ddf2259 100644
--- a/crates/diesel_models/src/schema.rs
+++ b/crates/diesel_models/src/schema.rs
@@ -1333,8 +1333,6 @@ diesel::table! {
         is_verified -> Bool,
         created_at -> Timestamp,
         last_modified_at -> Timestamp,
-        #[max_length = 64]
-        preferred_merchant_id -> Nullable<Varchar>,
         totp_status -> TotpStatus,
         totp_secret -> Nullable<Bytea>,
         totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs
index 410c541441b..c3dc45e6b62 100644
--- a/crates/diesel_models/src/schema_v2.rs
+++ b/crates/diesel_models/src/schema_v2.rs
@@ -1316,8 +1316,6 @@ diesel::table! {
         is_verified -> Bool,
         created_at -> Timestamp,
         last_modified_at -> Timestamp,
-        #[max_length = 64]
-        preferred_merchant_id -> Nullable<Varchar>,
         totp_status -> TotpStatus,
         totp_secret -> Nullable<Bytea>,
         totp_recovery_codes -> Nullable<Array<Nullable<Text>>>,
diff --git a/crates/diesel_models/src/user.rs b/crates/diesel_models/src/user.rs
index 582041ec42e..9f7b77dc5d6 100644
--- a/crates/diesel_models/src/user.rs
+++ b/crates/diesel_models/src/user.rs
@@ -18,7 +18,6 @@ pub struct User {
     pub is_verified: bool,
     pub created_at: PrimitiveDateTime,
     pub last_modified_at: PrimitiveDateTime,
-    pub preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
     pub totp_status: TotpStatus,
     pub totp_secret: Option<Encryption>,
     #[diesel(deserialize_as = OptionalDieselArray<Secret<String>>)]
@@ -38,7 +37,6 @@ pub struct UserNew {
     pub is_verified: bool,
     pub created_at: Option<PrimitiveDateTime>,
     pub last_modified_at: Option<PrimitiveDateTime>,
-    pub preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
     pub totp_status: TotpStatus,
     pub totp_secret: Option<Encryption>,
     pub totp_recovery_codes: Option<Vec<Secret<String>>>,
@@ -52,7 +50,6 @@ pub struct UserUpdateInternal {
     password: Option<Secret<String>>,
     is_verified: Option<bool>,
     last_modified_at: PrimitiveDateTime,
-    preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
     totp_status: Option<TotpStatus>,
     totp_secret: Option<Encryption>,
     totp_recovery_codes: Option<Vec<Secret<String>>>,
@@ -65,7 +62,6 @@ pub enum UserUpdate {
     AccountUpdate {
         name: Option<String>,
         is_verified: Option<bool>,
-        preferred_merchant_id: Option<common_utils::id_type::MerchantId>,
     },
     TotpUpdate {
         totp_status: Option<TotpStatus>,
@@ -86,22 +82,16 @@ impl From<UserUpdate> for UserUpdateInternal {
                 password: None,
                 is_verified: Some(true),
                 last_modified_at,
-                preferred_merchant_id: None,
                 totp_status: None,
                 totp_secret: None,
                 totp_recovery_codes: None,
                 last_password_modified_at: None,
             },
-            UserUpdate::AccountUpdate {
-                name,
-                is_verified,
-                preferred_merchant_id,
-            } => Self {
+            UserUpdate::AccountUpdate { name, is_verified } => Self {
                 name,
                 password: None,
                 is_verified,
                 last_modified_at,
-                preferred_merchant_id,
                 totp_status: None,
                 totp_secret: None,
                 totp_recovery_codes: None,
@@ -116,7 +106,6 @@ impl From<UserUpdate> for UserUpdateInternal {
                 password: None,
                 is_verified: None,
                 last_modified_at,
-                preferred_merchant_id: None,
                 totp_status,
                 totp_secret,
                 totp_recovery_codes,
@@ -127,7 +116,6 @@ impl From<UserUpdate> for UserUpdateInternal {
                 password: Some(password),
                 is_verified: None,
                 last_modified_at,
-                preferred_merchant_id: None,
                 last_password_modified_at: Some(last_modified_at),
                 totp_status: None,
                 totp_secret: None,
diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs
index c454e8b58bc..29be4d62aef 100644
--- a/crates/diesel_models/src/user_role.rs
+++ b/crates/diesel_models/src/user_role.rs
@@ -26,52 +26,53 @@ pub struct UserRole {
     pub version: enums::UserRoleVersion,
 }
 
-pub fn get_entity_id_and_type(user_role: &UserRole) -> (Option<String>, Option<EntityType>) {
-    match (user_role.version, user_role.role_id.as_str()) {
-        (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => (
-            user_role
-                .org_id
-                .clone()
-                .map(|org_id| org_id.get_string_repr().to_string()),
-            Some(EntityType::Organization),
-        ),
-        (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER)
-        | (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => (
-            user_role
-                .merchant_id
-                .clone()
-                .map(|merchant_id| merchant_id.get_string_repr().to_string()),
-            Some(EntityType::Internal),
-        ),
-        (enums::UserRoleVersion::V1, _) => (
-            user_role
-                .merchant_id
-                .clone()
-                .map(|merchant_id| merchant_id.get_string_repr().to_string()),
-            Some(EntityType::Merchant),
-        ),
-        (enums::UserRoleVersion::V2, _) => (user_role.entity_id.clone(), user_role.entity_type),
+impl UserRole {
+    pub fn get_entity_id_and_type(&self) -> Option<(String, EntityType)> {
+        match (self.version, self.role_id.as_str()) {
+            (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => {
+                let org_id = self.org_id.clone()?.get_string_repr().to_string();
+                Some((org_id, EntityType::Organization))
+            }
+            (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER)
+            | (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => {
+                let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string();
+                Some((merchant_id, EntityType::Internal))
+            }
+            (enums::UserRoleVersion::V1, _) => {
+                let merchant_id = self.merchant_id.clone()?.get_string_repr().to_string();
+                Some((merchant_id, EntityType::Merchant))
+            }
+            (enums::UserRoleVersion::V2, _) => self.entity_id.clone().zip(self.entity_type),
+        }
     }
 }
 
 impl Hash for UserRole {
     fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
-        let (entity_id, entity_type) = get_entity_id_and_type(self);
-
         self.user_id.hash(state);
-        entity_id.hash(state);
-        entity_type.hash(state);
+        if let Some((entity_id, entity_type)) = self.get_entity_id_and_type() {
+            entity_id.hash(state);
+            entity_type.hash(state);
+        }
     }
 }
 
 impl PartialEq for UserRole {
     fn eq(&self, other: &Self) -> bool {
-        let (self_entity_id, self_entity_type) = get_entity_id_and_type(self);
-        let (other_entity_id, other_entity_type) = get_entity_id_and_type(other);
-
-        self.user_id == other.user_id
-            && self_entity_id == other_entity_id
-            && self_entity_type == other_entity_type
+        match (
+            self.get_entity_id_and_type(),
+            other.get_entity_id_and_type(),
+        ) {
+            (
+                Some((self_entity_id, self_entity_type)),
+                Some((other_entity_id, other_entity_type)),
+            ) => {
+                self.user_id == other.user_id
+                    && self_entity_id == other_entity_id
+                    && self_entity_type == other_entity_type
+            }
+            _ => self.user_id == other.user_id,
+        }
     }
 }
 
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index 8e99a202dfe..0c4afbe4ecd 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -18,8 +18,6 @@ use diesel_models::{
     user_authentication_method::{UserAuthenticationMethodNew, UserAuthenticationMethodUpdate},
 };
 use error_stack::{report, ResultExt};
-#[cfg(feature = "email")]
-use external_services::email::EmailData;
 use masking::{ExposeInterface, PeekInterface, Secret};
 #[cfg(feature = "email")]
 use router_env::env;
@@ -64,7 +62,7 @@ pub async fn signup_with_merchant_id(
         .insert_user_and_merchant_in_db(state.clone())
         .await?;
 
-    let user_role = new_user
+    let _user_role = new_user
         .insert_org_level_user_role_in_db(
             state.clone(),
             common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
@@ -89,16 +87,10 @@ pub async fn signup_with_merchant_id(
         )
         .await;
 
-    let Some(merchant_id) = user_role.merchant_id else {
-        return Err(report!(UserErrors::InternalServerError)
-            .attach_printable("merchant_id not found for user_role"));
-    };
-
     logger::info!(?send_email_result);
     Ok(ApplicationResponse::Json(user_api::AuthorizeResponse {
         is_email_sent: send_email_result.is_ok(),
         user_id: user_from_db.get_user_id().to_string(),
-        merchant_id,
     }))
 }
 
@@ -195,7 +187,6 @@ pub async fn connect_account(
 
     if let Ok(found_user) = find_user {
         let user_from_db: domain::UserFromStorage = found_user.into();
-        let user_role = user_from_db.get_role_from_db(state.clone()).await?;
 
         let email_contents = email_types::MagicLink {
             recipient_email: domain::UserEmail::from_pii_email(user_from_db.get_email())?,
@@ -212,18 +203,12 @@ pub async fn connect_account(
                 state.conf.proxy.https_url.as_ref(),
             )
             .await;
-
-        let Some(merchant_id) = user_role.merchant_id else {
-            return Err(report!(UserErrors::InternalServerError)
-                .attach_printable("merchant_id not found for user_role"));
-        };
         logger::info!(?send_email_result);
 
         return Ok(ApplicationResponse::Json(
             user_api::ConnectAccountResponse {
                 is_email_sent: send_email_result.is_ok(),
                 user_id: user_from_db.get_user_id().to_string(),
-                merchant_id,
             },
         ));
     } else if find_user
@@ -245,7 +230,7 @@ pub async fn connect_account(
         let user_from_db = new_user
             .insert_user_and_merchant_in_db(state.clone())
             .await?;
-        let user_role = new_user
+        let _user_role = new_user
             .insert_org_level_user_role_in_db(
                 state.clone(),
                 common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
@@ -269,18 +254,12 @@ pub async fn connect_account(
             )
             .await;
 
-        let Some(merchant_id) = user_role.merchant_id else {
-            return Err(report!(UserErrors::InternalServerError)
-                .attach_printable("merchant_id not found for user_role"));
-        };
-
         logger::info!(?send_email_result);
 
         return Ok(ApplicationResponse::Json(
             user_api::ConnectAccountResponse {
                 is_email_sent: send_email_result.is_ok(),
                 user_id: user_from_db.get_user_id().to_string(),
-                merchant_id,
             },
         ));
     } else {
@@ -512,15 +491,19 @@ pub async fn invite_multiple_user(
             .attach_printable("Number of invite requests must not exceed 10");
     }
 
-    let responses = futures::future::join_all(requests.iter().map(|request| async {
-        match handle_invitation(&state, &user_from_token, request, &req_state, &auth_id).await {
+    let responses = futures::future::join_all(requests.into_iter().map(|request| async {
+        match handle_invitation(&state, &user_from_token, &request, &req_state, &auth_id).await {
             Ok(response) => response,
-            Err(error) => InviteMultipleUserResponse {
-                email: request.email.clone(),
-                is_email_sent: false,
-                password: None,
-                error: Some(error.current_context().get_error_message().to_string()),
-            },
+            Err(error) => {
+                logger::error!(invite_error=?error);
+
+                InviteMultipleUserResponse {
+                    email: request.email,
+                    is_email_sent: false,
+                    password: None,
+                    error: Some(error.current_context().get_error_message().to_string()),
+                }
+            }
         }
     }))
     .await;
@@ -570,6 +553,7 @@ async fn handle_invitation(
             user_from_token,
             request,
             invitee_user.into(),
+            role_info,
             auth_id,
         )
         .await
@@ -579,8 +563,15 @@ async fn handle_invitation(
         .err()
         .unwrap_or(false)
     {
-        handle_new_user_invitation(state, user_from_token, request, req_state.clone(), auth_id)
-            .await
+        handle_new_user_invitation(
+            state,
+            user_from_token,
+            request,
+            role_info,
+            req_state.clone(),
+            auth_id,
+        )
+        .await
     } else {
         Err(UserErrors::InternalServerError.into())
     }
@@ -592,6 +583,7 @@ async fn handle_existing_user_invitation(
     user_from_token: &auth::UserFromToken,
     request: &user_api::InviteUserRequest,
     invitee_user_from_db: domain::UserFromStorage,
+    role_info: roles::RoleInfo,
     auth_id: &Option<String>,
 ) -> UserResult<InviteMultipleUserResponse> {
     let now = common_utils::date_time::now();
@@ -642,24 +634,66 @@ async fn handle_existing_user_invitation(
         last_modified_by: user_from_token.user_id.clone(),
         created_at: now,
         last_modified: now,
-        entity: domain::MerchantLevel {
-            org_id: user_from_token.org_id.clone(),
-            merchant_id: user_from_token.merchant_id.clone(),
-        },
-    }
-    .insert_in_v1_and_v2(state)
-    .await?;
+        entity: domain::NoLevel,
+    };
+
+    let _user_role = match role_info.get_entity_type() {
+        EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Merchant => {
+            user_role
+                .add_entity(domain::MerchantLevel {
+                    org_id: user_from_token.org_id.clone(),
+                    merchant_id: user_from_token.merchant_id.clone(),
+                })
+                .insert_in_v1_and_v2(state)
+                .await?
+        }
+        EntityType::Profile => {
+            let profile_id = user_from_token
+                .profile_id
+                .clone()
+                .ok_or(UserErrors::InternalServerError)?;
+            user_role
+                .add_entity(domain::ProfileLevel {
+                    org_id: user_from_token.org_id.clone(),
+                    merchant_id: user_from_token.merchant_id.clone(),
+                    profile_id: profile_id.clone(),
+                })
+                .insert_in_v2(state)
+                .await?
+        }
+    };
 
     let is_email_sent;
     #[cfg(feature = "email")]
     {
         let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
-        let email_contents = email_types::InviteRegisteredUser {
+        let entity = match role_info.get_entity_type() {
+            EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Merchant => email_types::Entity {
+                entity_id: user_from_token.merchant_id.get_string_repr().to_owned(),
+                entity_type: EntityType::Merchant,
+            },
+            EntityType::Profile => {
+                let profile_id = user_from_token
+                    .profile_id
+                    .clone()
+                    .ok_or(UserErrors::InternalServerError)?;
+                email_types::Entity {
+                    entity_id: profile_id.get_string_repr().to_owned(),
+                    entity_type: EntityType::Profile,
+                }
+            }
+        };
+
+        let email_contents = email_types::InviteUser {
             recipient_email: invitee_email,
             user_name: domain::UserName::new(invitee_user_from_db.get_name())?,
             settings: state.conf.clone(),
             subject: "You have been invited to join Hyperswitch Community!",
-            merchant_id: user_from_token.merchant_id.clone(),
+            entity,
             auth_id: auth_id.clone(),
         };
 
@@ -692,6 +726,7 @@ async fn handle_new_user_invitation(
     state: &SessionState,
     user_from_token: &auth::UserFromToken,
     request: &user_api::InviteUserRequest,
+    role_info: roles::RoleInfo,
     req_state: ReqState,
     auth_id: &Option<String>,
 ) -> UserResult<InviteMultipleUserResponse> {
@@ -718,13 +753,36 @@ async fn handle_new_user_invitation(
         last_modified_by: user_from_token.user_id.clone(),
         created_at: now,
         last_modified: now,
-        entity: domain::MerchantLevel {
-            merchant_id: user_from_token.merchant_id.clone(),
-            org_id: user_from_token.org_id.clone(),
-        },
-    }
-    .insert_in_v1_and_v2(state)
-    .await?;
+        entity: domain::NoLevel,
+    };
+
+    let _user_role = match role_info.get_entity_type() {
+        EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+        EntityType::Merchant => {
+            user_role
+                .add_entity(domain::MerchantLevel {
+                    org_id: user_from_token.org_id.clone(),
+                    merchant_id: user_from_token.merchant_id.clone(),
+                })
+                .insert_in_v1_and_v2(state)
+                .await?
+        }
+        EntityType::Profile => {
+            let profile_id = user_from_token
+                .profile_id
+                .clone()
+                .ok_or(UserErrors::InternalServerError)?;
+            user_role
+                .add_entity(domain::ProfileLevel {
+                    org_id: user_from_token.org_id.clone(),
+                    merchant_id: user_from_token.merchant_id.clone(),
+                    profile_id: profile_id.clone(),
+                })
+                .insert_in_v2(state)
+                .await?
+        }
+    };
 
     let is_email_sent;
 
@@ -734,18 +792,39 @@ async fn handle_new_user_invitation(
         // Will be adding actual usage for this variable later
         let _ = req_state.clone();
         let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
-        let email_contents: Box<dyn EmailData + Send + 'static> =
-            Box::new(email_types::InviteRegisteredUser {
-                recipient_email: invitee_email,
-                user_name: domain::UserName::new(new_user.get_name())?,
-                settings: state.conf.clone(),
-                subject: "You have been invited to join Hyperswitch Community!",
-                merchant_id: user_from_token.merchant_id.clone(),
-                auth_id: auth_id.clone(),
-            });
+        let entity = match role_info.get_entity_type() {
+            EntityType::Internal => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Organization => return Err(UserErrors::InvalidRoleId.into()),
+            EntityType::Merchant => email_types::Entity {
+                entity_id: user_from_token.merchant_id.get_string_repr().to_owned(),
+                entity_type: EntityType::Merchant,
+            },
+            EntityType::Profile => {
+                let profile_id = user_from_token
+                    .profile_id
+                    .clone()
+                    .ok_or(UserErrors::InternalServerError)?;
+                email_types::Entity {
+                    entity_id: profile_id.get_string_repr().to_owned(),
+                    entity_type: EntityType::Profile,
+                }
+            }
+        };
+
+        let email_contents = email_types::InviteUser {
+            recipient_email: invitee_email,
+            user_name: domain::UserName::new(new_user.get_name())?,
+            settings: state.conf.clone(),
+            subject: "You have been invited to join Hyperswitch Community!",
+            entity,
+            auth_id: auth_id.clone(),
+        };
         let send_email_result = state
             .email_client
-            .compose_and_send_email(email_contents, state.conf.proxy.https_url.as_ref())
+            .compose_and_send_email(
+                Box::new(email_contents),
+                state.conf.proxy.https_url.as_ref(),
+            )
             .await;
         logger::info!(?send_email_result);
         is_email_sent = send_email_result.is_ok();
@@ -803,40 +882,66 @@ pub async fn resend_invite(
             }
         })?
         .into();
-    let user_role = state
+
+    let user_role = match state
         .store
-        .find_user_role_by_user_id_merchant_id(
+        .find_user_role_by_user_id_and_lineage(
             user.get_user_id(),
+            &user_from_token.org_id,
             &user_from_token.merchant_id,
-            UserRoleVersion::V1,
+            user_from_token.profile_id.as_ref(),
+            UserRoleVersion::V2,
         )
         .await
-        .map_err(|e| {
-            if e.current_context().is_db_not_found() {
-                e.change_context(UserErrors::InvalidRoleOperation)
-                    .attach_printable(format!(
-                        "User role with user_id = {} and merchant_id = {:?} is not found",
-                        user.get_user_id(),
-                        user_from_token.merchant_id
-                    ))
+    {
+        Ok(user_role) => Some(user_role),
+        Err(err) => {
+            if err.current_context().is_db_not_found() {
+                None
             } else {
-                e.change_context(UserErrors::InternalServerError)
+                return Err(report!(UserErrors::InternalServerError));
             }
-        })?;
+        }
+    };
+
+    let user_role = match user_role {
+        Some(user_role) => user_role,
+        None => state
+            .store
+            .find_user_role_by_user_id_and_lineage(
+                user.get_user_id(),
+                &user_from_token.org_id,
+                &user_from_token.merchant_id,
+                user_from_token.profile_id.as_ref(),
+                UserRoleVersion::V1,
+            )
+            .await
+            .to_not_found_response(UserErrors::InvalidRoleOperationWithMessage(
+                "User not found in records".to_string(),
+            ))?,
+    };
 
     if !matches!(user_role.status, UserStatus::InvitationSent) {
         return Err(report!(UserErrors::InvalidRoleOperation))
             .attach_printable("User status is not InvitationSent".to_string());
     }
 
+    let (entity_id, entity_type) = user_role
+        .get_entity_id_and_type()
+        .ok_or(UserErrors::InternalServerError)?;
+
     let email_contents = email_types::InviteUser {
         recipient_email: invitee_email,
         user_name: domain::UserName::new(user.get_name())?,
         settings: state.conf.clone(),
         subject: "You have been invited to join Hyperswitch Community!",
-        merchant_id: user_from_token.merchant_id,
-        auth_id,
+        entity: email_types::Entity {
+            entity_id,
+            entity_type,
+        },
+        auth_id: auth_id.clone(),
     };
+
     state
         .email_client
         .compose_and_send_email(
@@ -878,36 +983,25 @@ pub async fn accept_invite_from_email_token_only_flow(
         return Err(UserErrors::LinkInvalid.into());
     }
 
-    let merchant_id = email_token
-        .get_merchant_id()
-        .ok_or(UserErrors::LinkInvalid)?;
-
-    let key_manager_state = &(&state).into();
+    let entity = email_token.get_entity().ok_or(UserErrors::LinkInvalid)?;
 
-    let key_store = state
-        .store
-        .get_merchant_key_store_by_merchant_id(
-            key_manager_state,
-            merchant_id,
-            &state.store.get_master_key().to_vec().into(),
+    let (org_id, merchant_id, profile_id) =
+        utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite(
+            &state,
+            &user_token.user_id,
+            entity.entity_id.clone(),
+            entity.entity_type,
         )
         .await
-        .change_context(UserErrors::InternalServerError)
-        .attach_printable("merchant_key_store not found")?;
-
-    let merchant_account = state
-        .store
-        .find_merchant_account_by_merchant_id(key_manager_state, merchant_id, &key_store)
-        .await
-        .change_context(UserErrors::InternalServerError)
-        .attach_printable("merchant_account not found")?;
+        .change_context(UserErrors::InternalServerError)?
+        .ok_or(UserErrors::InternalServerError)?;
 
     let (update_v1_result, update_v2_result) = utils::user_role::update_v1_and_v2_user_roles_in_db(
         &state,
         user_from_db.get_user_id(),
-        &merchant_account.organization_id,
-        merchant_id,
-        None,
+        &org_id,
+        &merchant_id,
+        profile_id.as_ref(),
         UserRoleUpdate::UpdateStatus {
             status: UserStatus::Active,
             modified_by: user_from_db.get_user_id().to_owned(),
@@ -951,14 +1045,7 @@ pub async fn accept_invite_from_email_token_only_flow(
     )?;
     let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
 
-    let user_role = user_from_db
-        .get_preferred_or_active_user_role_from_db(&state)
-        .await
-        .change_context(UserErrors::InternalServerError)?;
-
-    let token = next_flow
-        .get_token_with_user_role(&state, &user_role)
-        .await?;
+    let token = next_flow.get_token(&state).await?;
 
     let response = user_api::TokenResponse {
         token: token.clone(),
@@ -1534,28 +1621,9 @@ pub async fn update_user_details(
 
     let name = req.name.map(domain::UserName::new).transpose()?;
 
-    if let Some(ref preferred_merchant_id) = req.preferred_merchant_id {
-        let _ = state
-            .store
-            .find_user_role_by_user_id_merchant_id(
-                user.get_user_id(),
-                preferred_merchant_id,
-                UserRoleVersion::V1,
-            )
-            .await
-            .map_err(|e| {
-                if e.current_context().is_db_not_found() {
-                    e.change_context(UserErrors::MerchantIdNotFound)
-                } else {
-                    e.change_context(UserErrors::InternalServerError)
-                }
-            })?;
-    }
-
     let user_update = storage_user::UserUpdate::AccountUpdate {
-        name: name.map(|x| x.get_secret().expose()),
+        name: name.map(|name| name.get_secret().expose()),
         is_verified: None,
-        preferred_merchant_id: req.preferred_merchant_id,
     };
 
     state
@@ -2283,23 +2351,35 @@ pub async fn list_orgs_for_user(
     state: SessionState,
     user_from_token: auth::UserFromToken,
 ) -> UserResponse<Vec<user_api::ListOrgsForUserResponse>> {
-    let orgs = state
-        .store
-        .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
-            user_id: user_from_token.user_id.as_str(),
-            org_id: None,
-            merchant_id: None,
-            profile_id: None,
-            entity_id: None,
-            version: None,
-            status: Some(UserStatus::Active),
-            limit: None,
-        })
-        .await
-        .change_context(UserErrors::InternalServerError)?
-        .into_iter()
-        .filter_map(|user_role| user_role.org_id)
-        .collect::<HashSet<_>>();
+    let role_info = roles::RoleInfo::from_role_id(
+        &state,
+        &user_from_token.role_id,
+        &user_from_token.merchant_id,
+        &user_from_token.org_id,
+    )
+    .await
+    .change_context(UserErrors::InternalServerError)?;
+
+    let orgs = match role_info.get_entity_type() {
+        EntityType::Internal => return Err(UserErrors::InvalidRoleOperation.into()),
+        EntityType::Organization | EntityType::Merchant | EntityType::Profile => state
+            .store
+            .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                user_id: user_from_token.user_id.as_str(),
+                org_id: None,
+                merchant_id: None,
+                profile_id: None,
+                entity_id: None,
+                version: None,
+                status: Some(UserStatus::Active),
+                limit: None,
+            })
+            .await
+            .change_context(UserErrors::InternalServerError)?
+            .into_iter()
+            .filter_map(|user_role| user_role.org_id)
+            .collect::<HashSet<_>>(),
+    };
 
     let resp = futures::future::try_join_all(
         orgs.iter()
@@ -2333,48 +2413,50 @@ pub async fn list_merchants_for_user_in_org(
     )
     .await
     .change_context(UserErrors::InternalServerError)?;
-    let merchant_accounts = if role_info.get_entity_type() == EntityType::Organization {
-        state
+    let merchant_accounts = match role_info.get_entity_type() {
+        EntityType::Organization | EntityType::Internal => state
             .store
             .list_merchant_accounts_by_organization_id(
                 &(&state).into(),
                 user_from_token.org_id.get_string_repr(),
             )
             .await
-            .change_context(UserErrors::InternalServerError)?
-            .into_iter()
-            .map(
-                |merchant_account| user_api::ListMerchantsForUserInOrgResponse {
-                    merchant_name: merchant_account.merchant_name.clone(),
-                    merchant_id: merchant_account.get_id().to_owned(),
-                },
-            )
-            .collect::<Vec<_>>()
-    } else {
-        let merchant_ids = state
-            .store
-            .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
-                user_id: user_from_token.user_id.as_str(),
-                org_id: Some(&user_from_token.org_id),
-                merchant_id: None,
-                profile_id: None,
-                entity_id: None,
-                version: None,
-                status: Some(UserStatus::Active),
-                limit: None,
-            })
-            .await
-            .change_context(UserErrors::InternalServerError)?
-            .into_iter()
-            .filter_map(|user_role| user_role.merchant_id)
-            .collect::<HashSet<_>>()
-            .into_iter()
-            .collect();
-        state
-            .store
-            .list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
-            .await
-            .change_context(UserErrors::InternalServerError)?
+            .change_context(UserErrors::InternalServerError)?,
+        EntityType::Merchant | EntityType::Profile => {
+            let merchant_ids = state
+                .store
+                .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                    user_id: user_from_token.user_id.as_str(),
+                    org_id: Some(&user_from_token.org_id),
+                    merchant_id: None,
+                    profile_id: None,
+                    entity_id: None,
+                    version: None,
+                    status: Some(UserStatus::Active),
+                    limit: None,
+                })
+                .await
+                .change_context(UserErrors::InternalServerError)?
+                .into_iter()
+                .filter_map(|user_role| user_role.merchant_id)
+                .collect::<HashSet<_>>()
+                .into_iter()
+                .collect();
+
+            state
+                .store
+                .list_multiple_merchant_accounts(&(&state).into(), merchant_ids)
+                .await
+                .change_context(UserErrors::InternalServerError)?
+        }
+    };
+
+    if merchant_accounts.is_empty() {
+        Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?;
+    }
+
+    Ok(ApplicationResponse::Json(
+        merchant_accounts
             .into_iter()
             .map(
                 |merchant_account| user_api::ListMerchantsForUserInOrgResponse {
@@ -2382,14 +2464,8 @@ pub async fn list_merchants_for_user_in_org(
                     merchant_id: merchant_account.get_id().to_owned(),
                 },
             )
-            .collect::<Vec<_>>()
-    };
-
-    if merchant_accounts.is_empty() {
-        Err(UserErrors::InternalServerError).attach_printable("No merchant found for a user")?;
-    }
-
-    Ok(ApplicationResponse::Json(merchant_accounts))
+            .collect::<Vec<_>>(),
+    ))
 }
 
 pub async fn list_profiles_for_user_in_org_and_merchant_account(
@@ -2415,27 +2491,17 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
         )
         .await
         .change_context(UserErrors::InternalServerError)?;
-    let user_role_level = role_info.get_entity_type();
-    let profiles =
-        if user_role_level == EntityType::Organization || user_role_level == EntityType::Merchant {
-            state
-                .store
-                .list_business_profile_by_merchant_id(
-                    key_manager_state,
-                    &key_store,
-                    &user_from_token.merchant_id,
-                )
-                .await
-                .change_context(UserErrors::InternalServerError)?
-                .into_iter()
-                .map(
-                    |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
-                        profile_id: profile.get_id().to_owned(),
-                        profile_name: profile.profile_name,
-                    },
-                )
-                .collect::<Vec<_>>()
-        } else {
+    let profiles = match role_info.get_entity_type() {
+        EntityType::Organization | EntityType::Merchant | EntityType::Internal => state
+            .store
+            .list_business_profile_by_merchant_id(
+                key_manager_state,
+                &key_store,
+                &user_from_token.merchant_id,
+            )
+            .await
+            .change_context(UserErrors::InternalServerError)?,
+        EntityType::Profile => {
             let profile_ids = state
                 .store
                 .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
@@ -2463,6 +2529,15 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
             }))
             .await
             .change_context(UserErrors::InternalServerError)?
+        }
+    };
+
+    if profiles.is_empty() {
+        Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?;
+    }
+
+    Ok(ApplicationResponse::Json(
+        profiles
             .into_iter()
             .map(
                 |profile| user_api::ListProfilesForUserInOrgAndMerchantAccountResponse {
@@ -2470,14 +2545,8 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account(
                     profile_name: profile.profile_name,
                 },
             )
-            .collect::<Vec<_>>()
-        };
-
-    if profiles.is_empty() {
-        Err(UserErrors::InternalServerError).attach_printable("No profile found for a user")?;
-    }
-
-    Ok(ApplicationResponse::Json(profiles))
+            .collect::<Vec<_>>(),
+    ))
 }
 
 pub async fn switch_org_for_user(
diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs
index 5ee71d0e509..9d57702e3be 100644
--- a/crates/router/src/core/user_role.rs
+++ b/crates/router/src/core/user_role.rs
@@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet};
 use api_models::{user as user_api, user_role as user_role_api};
 use diesel_models::{
     enums::{UserRoleVersion, UserStatus},
-    user_role::{get_entity_id_and_type, UserRoleUpdate},
+    user_role::UserRoleUpdate,
 };
 use error_stack::{report, ResultExt};
 use once_cell::sync::Lazy;
@@ -289,7 +289,59 @@ pub async fn accept_invitation(
         }))
         .await;
 
-    if update_result.iter().all(Result::is_err) {
+    if update_result.is_empty() || update_result.iter().all(Result::is_err) {
+        return Err(UserErrors::MerchantIdNotFound.into());
+    }
+
+    Ok(ApplicationResponse::StatusOk)
+}
+
+pub async fn accept_invitations_v2(
+    state: SessionState,
+    user_from_token: auth::UserFromToken,
+    req: user_role_api::AcceptInvitationsV2Request,
+) -> UserResponse<()> {
+    let lineages = futures::future::try_join_all(req.into_iter().map(|entity| {
+        utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite(
+            &state,
+            &user_from_token.user_id,
+            entity.entity_id,
+            entity.entity_type,
+        )
+    }))
+    .await?
+    .into_iter()
+    .flatten()
+    .collect::<Vec<_>>();
+
+    let update_results = futures::future::join_all(lineages.iter().map(
+        |(org_id, merchant_id, profile_id)| async {
+            let (update_v1_result, update_v2_result) =
+                utils::user_role::update_v1_and_v2_user_roles_in_db(
+                    &state,
+                    user_from_token.user_id.as_str(),
+                    org_id,
+                    merchant_id,
+                    profile_id.as_ref(),
+                    UserRoleUpdate::UpdateStatus {
+                        status: UserStatus::Active,
+                        modified_by: user_from_token.user_id.clone(),
+                    },
+                )
+                .await;
+
+            if update_v1_result.is_err_and(|err| !err.current_context().is_db_not_found())
+                || update_v2_result.is_err_and(|err| !err.current_context().is_db_not_found())
+            {
+                Err(report!(UserErrors::InternalServerError))
+            } else {
+                Ok(())
+            }
+        },
+    ))
+    .await;
+
+    if update_results.is_empty() || update_results.iter().all(Result::is_err) {
         return Err(UserErrors::MerchantIdNotFound.into());
     }
 
@@ -333,7 +385,7 @@ pub async fn merchant_select_token_only_flow(
         }))
         .await;
 
-    if update_result.iter().all(Result::is_err) {
+    if update_result.is_empty() || update_result.iter().all(Result::is_err) {
         return Err(UserErrors::MerchantIdNotFound.into());
     }
 
@@ -344,18 +396,80 @@ pub async fn merchant_select_token_only_flow(
         .change_context(UserErrors::InternalServerError)?
         .into();
 
-    let user_role = user_from_db
-        .get_preferred_or_active_user_role_from_db(&state)
+    let current_flow =
+        domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?;
+    let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
+
+    let token = next_flow.get_token(&state).await?;
+
+    let response = user_api::TokenResponse {
+        token: token.clone(),
+        token_type: next_flow.get_flow().into(),
+    };
+    auth::cookies::set_cookie_response(response, token)
+}
+
+pub async fn accept_invitations_pre_auth(
+    state: SessionState,
+    user_token: auth::UserFromSinglePurposeToken,
+    req: user_role_api::AcceptInvitationsPreAuthRequest,
+) -> UserResponse<user_api::TokenResponse> {
+    let lineages = futures::future::try_join_all(req.into_iter().map(|entity| {
+        utils::user_role::get_lineage_for_user_id_and_entity_for_accepting_invite(
+            &state,
+            &user_token.user_id,
+            entity.entity_id,
+            entity.entity_type,
+        )
+    }))
+    .await?
+    .into_iter()
+    .flatten()
+    .collect::<Vec<_>>();
+
+    let update_results = futures::future::join_all(lineages.iter().map(
+        |(org_id, merchant_id, profile_id)| async {
+            let (update_v1_result, update_v2_result) =
+                utils::user_role::update_v1_and_v2_user_roles_in_db(
+                    &state,
+                    user_token.user_id.as_str(),
+                    org_id,
+                    merchant_id,
+                    profile_id.as_ref(),
+                    UserRoleUpdate::UpdateStatus {
+                        status: UserStatus::Active,
+                        modified_by: user_token.user_id.clone(),
+                    },
+                )
+                .await;
+
+            if update_v1_result.is_err_and(|err| !err.current_context().is_db_not_found())
+                || update_v2_result.is_err_and(|err| !err.current_context().is_db_not_found())
+            {
+                Err(report!(UserErrors::InternalServerError))
+            } else {
+                Ok(())
+            }
+        },
+    ))
+    .await;
+
+    if update_results.is_empty() || update_results.iter().all(Result::is_err) {
+        return Err(UserErrors::MerchantIdNotFound.into());
+    }
+
+    let user_from_db: domain::UserFromStorage = state
+        .global_store
+        .find_user_by_id(user_token.user_id.as_str())
         .await
-        .change_context(UserErrors::InternalServerError)?;
+        .change_context(UserErrors::InternalServerError)?
+        .into();
 
     let current_flow =
         domain::CurrentFlow::new(user_token, domain::SPTFlow::MerchantSelect.into())?;
     let next_flow = current_flow.next(user_from_db.clone(), &state).await?;
 
-    let token = next_flow
-        .get_token_with_user_role(&state, &user_role)
-        .await?;
+    let token = next_flow.get_token(&state).await?;
 
     let response = user_api::TokenResponse {
         token: token.clone(),
@@ -711,14 +825,12 @@ pub async fn list_invitations_for_user(
         .collect::<HashSet<_>>()
         .into_iter()
         .filter_map(|user_role| {
-            let (entity_id, entity_type) = get_entity_id_and_type(&user_role);
-            entity_id.zip(entity_type).map(|(entity_id, entity_type)| {
-                user_role_api::ListInvitationForUserResponse {
-                    entity_id,
-                    entity_type,
-                    entity_name: None,
-                    role_id: user_role.role_id,
-                }
+            let (entity_id, entity_type) = user_role.get_entity_id_and_type()?;
+            Some(user_role_api::ListInvitationForUserResponse {
+                entity_id,
+                entity_type,
+                entity_name: None,
+                role_id: user_role.role_id,
             })
         })
         .collect();
diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs
index b8c5001d910..c9244eb12b4 100644
--- a/crates/router/src/db/user.rs
+++ b/crates/router/src/db/user.rs
@@ -159,7 +159,6 @@ impl UserInterface for MockDb {
             is_verified: user_data.is_verified,
             created_at: user_data.created_at.unwrap_or(time_now),
             last_modified_at: user_data.created_at.unwrap_or(time_now),
-            preferred_merchant_id: user_data.preferred_merchant_id,
             totp_status: user_data.totp_status,
             totp_secret: user_data.totp_secret,
             totp_recovery_codes: user_data.totp_recovery_codes,
@@ -218,16 +217,9 @@ impl UserInterface for MockDb {
                         is_verified: true,
                         ..user.to_owned()
                     },
-                    storage::UserUpdate::AccountUpdate {
-                        name,
-                        is_verified,
-                        preferred_merchant_id,
-                    } => storage::User {
+                    storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
                         is_verified: is_verified.unwrap_or(user.is_verified),
-                        preferred_merchant_id: preferred_merchant_id
-                            .clone()
-                            .or(user.preferred_merchant_id.clone()),
                         ..user.to_owned()
                     },
                     storage::UserUpdate::TotpUpdate {
@@ -273,16 +265,9 @@ impl UserInterface for MockDb {
                         is_verified: true,
                         ..user.to_owned()
                     },
-                    storage::UserUpdate::AccountUpdate {
-                        name,
-                        is_verified,
-                        preferred_merchant_id,
-                    } => storage::User {
+                    storage::UserUpdate::AccountUpdate { name, is_verified } => storage::User {
                         name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
                         is_verified: is_verified.unwrap_or(user.is_verified),
-                        preferred_merchant_id: preferred_merchant_id
-                            .clone()
-                            .or(user.preferred_merchant_id.clone()),
                         ..user.to_owned()
                     },
                     storage::UserUpdate::TotpUpdate {
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index bd05740acae..04d833ab985 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1853,9 +1853,22 @@ impl User {
                     web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)),
                 )
                 .service(
-                    web::resource("/invite/accept")
-                        .route(web::post().to(merchant_select))
-                        .route(web::put().to(accept_invitation)),
+                    web::scope("/invite/accept")
+                        .service(
+                            web::resource("")
+                                .route(web::post().to(merchant_select))
+                                .route(web::put().to(accept_invitation)),
+                        )
+                        .service(
+                            web::scope("/v2")
+                                .service(
+                                    web::resource("").route(web::post().to(accept_invitations_v2)),
+                                )
+                                .service(
+                                    web::resource("/pre_auth")
+                                        .route(web::post().to(accept_invitations_pre_auth)),
+                                ),
+                        ),
                 )
                 .service(web::resource("/update_role").route(web::post().to(update_user_role)))
                 .service(web::resource("/delete").route(web::delete().to(delete_user_role))),
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index c6160497918..2b2a1be0878 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -263,7 +263,9 @@ impl From<Flow> for ApiIdentifier {
             | Flow::GetAuthorizationInfo
             | Flow::GetRolesInfo
             | Flow::AcceptInvitation
+            | Flow::AcceptInvitationsV2
             | Flow::MerchantSelect
+            | Flow::AcceptInvitationsPreAuth
             | Flow::DeleteUserRole
             | Flow::CreateRole
             | Flow::UpdateRole
diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs
index 61250774c10..35c9098edd2 100644
--- a/crates/router/src/routes/user_role.rs
+++ b/crates/router/src/routes/user_role.rs
@@ -168,6 +168,25 @@ pub async fn accept_invitation(
     .await
 }
 
+pub async fn accept_invitations_v2(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_role_api::AcceptInvitationsV2Request>,
+) -> HttpResponse {
+    let flow = Flow::AcceptInvitationsV2;
+    let payload = json_payload.into_inner();
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload,
+        |state, user, req_body, _| user_role_core::accept_invitations_v2(state, user, req_body),
+        &auth::DashboardNoPermissionAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub async fn merchant_select(
     state: web::Data<AppState>,
     req: HttpRequest,
@@ -189,6 +208,27 @@ pub async fn merchant_select(
     .await
 }
 
+pub async fn accept_invitations_pre_auth(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    json_payload: web::Json<user_role_api::AcceptInvitationsPreAuthRequest>,
+) -> HttpResponse {
+    let flow = Flow::AcceptInvitationsPreAuth;
+    let payload = json_payload.into_inner();
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload,
+        |state, user, req_body, _| async move {
+            user_role_core::accept_invitations_pre_auth(state, user, req_body).await
+        },
+        &auth::SinglePurposeJWTAuth(TokenPurpose::AcceptInvite),
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 pub async fn delete_user_role(
     state: web::Data<AppState>,
     req: HttpRequest,
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 3a17966fac3..6ed03e1ea06 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -1,4 +1,5 @@
 use api_models::user::dashboard_metadata::ProdIntent;
+use common_enums::EntityType;
 use common_utils::{
     errors::{self, CustomResult},
     pii,
@@ -151,15 +152,31 @@ Email         : {user_email}
 #[derive(serde::Serialize, serde::Deserialize)]
 pub struct EmailToken {
     email: String,
-    merchant_id: Option<common_utils::id_type::MerchantId>,
     flow: domain::Origin,
     exp: u64,
+    entity: Option<Entity>,
+}
+
+#[derive(serde::Serialize, serde::Deserialize, Clone)]
+pub struct Entity {
+    pub entity_id: String,
+    pub entity_type: EntityType,
+}
+
+impl Entity {
+    pub fn get_entity_type(&self) -> EntityType {
+        self.entity_type
+    }
+
+    pub fn get_entity_id(&self) -> &str {
+        &self.entity_id
+    }
 }
 
 impl EmailToken {
     pub async fn new_token(
         email: domain::UserEmail,
-        merchant_id: Option<common_utils::id_type::MerchantId>,
+        entity: Option<Entity>,
         flow: domain::Origin,
         settings: &configs::Settings,
     ) -> CustomResult<String, UserErrors> {
@@ -167,9 +184,9 @@ impl EmailToken {
         let exp = jwt::generate_exp(expiration_duration)?.as_secs();
         let token_payload = Self {
             email: email.get_secret().expose(),
-            merchant_id,
             flow,
             exp,
+            entity,
         };
         jwt::generate_jwt(&token_payload, settings).await
     }
@@ -178,8 +195,8 @@ impl EmailToken {
         pii::Email::try_from(self.email.clone())
     }
 
-    pub fn get_merchant_id(&self) -> Option<&common_utils::id_type::MerchantId> {
-        self.merchant_id.as_ref()
+    pub fn get_entity(&self) -> Option<&Entity> {
+        self.entity.as_ref()
     }
 
     pub fn get_flow(&self) -> domain::Origin {
@@ -320,13 +337,12 @@ impl EmailData for MagicLink {
     }
 }
 
-// TODO: Deprecate this and use InviteRegisteredUser for new invites
 pub struct InviteUser {
     pub recipient_email: domain::UserEmail,
     pub user_name: domain::UserName,
     pub settings: std::sync::Arc<configs::Settings>,
     pub subject: &'static str,
-    pub merchant_id: common_utils::id_type::MerchantId,
+    pub entity: Entity,
     pub auth_id: Option<String>,
 }
 
@@ -335,48 +351,7 @@ impl EmailData for InviteUser {
     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
         let token = EmailToken::new_token(
             self.recipient_email.clone(),
-            Some(self.merchant_id.clone()),
-            domain::Origin::ResetPassword,
-            &self.settings,
-        )
-        .await
-        .change_context(EmailError::TokenGenerationFailure)?;
-
-        let invite_user_link = get_link_with_token(
-            &self.settings.user.base_url,
-            token,
-            "set_password",
-            &self.auth_id,
-        );
-
-        let body = html::get_html_body(EmailBody::InviteUser {
-            link: invite_user_link,
-            user_name: self.user_name.clone().get_secret().expose(),
-        });
-
-        Ok(EmailContents {
-            subject: self.subject.to_string(),
-            body: external_services::email::IntermediateString::new(body),
-            recipient: self.recipient_email.clone().into_inner(),
-        })
-    }
-}
-
-pub struct InviteRegisteredUser {
-    pub recipient_email: domain::UserEmail,
-    pub user_name: domain::UserName,
-    pub settings: std::sync::Arc<configs::Settings>,
-    pub subject: &'static str,
-    pub merchant_id: common_utils::id_type::MerchantId,
-    pub auth_id: Option<String>,
-}
-
-#[async_trait::async_trait]
-impl EmailData for InviteRegisteredUser {
-    async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
-        let token = EmailToken::new_token(
-            self.recipient_email.clone(),
-            Some(self.merchant_id.clone()),
+            Some(self.entity.clone()),
             domain::Origin::AcceptInvitationFromEmail,
             &self.settings,
         )
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index f41d7157eec..9f73276c280 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -706,7 +706,6 @@ impl TryFrom<NewUser> for storage_user::UserNew {
             is_verified: false,
             created_at: Some(now),
             last_modified_at: Some(now),
-            preferred_merchant_id: None,
             totp_status: TotpStatus::NotSet,
             totp_secret: None,
             totp_recovery_codes: None,
@@ -934,10 +933,6 @@ impl UserFromStorage {
         Ok(days_left_for_password_rotate.whole_days() < 0)
     }
 
-    pub fn get_preferred_merchant_id(&self) -> Option<id_type::MerchantId> {
-        self.0.preferred_merchant_id.clone()
-    }
-
     pub async fn get_role_from_db_by_merchant_id(
         &self,
         state: &SessionState,
@@ -953,29 +948,6 @@ impl UserFromStorage {
             .await
     }
 
-    pub async fn get_preferred_or_active_user_role_from_db(
-        &self,
-        state: &SessionState,
-    ) -> CustomResult<UserRole, errors::StorageError> {
-        if let Some(preferred_merchant_id) = self.get_preferred_merchant_id() {
-            self.get_role_from_db_by_merchant_id(state, &preferred_merchant_id)
-                .await
-        } else {
-            state
-                .store
-                .list_user_roles_by_user_id_and_version(&self.0.user_id, UserRoleVersion::V1)
-                .await?
-                .into_iter()
-                .find(|role| role.status == UserStatus::Active)
-                .ok_or(
-                    errors::StorageError::ValueNotFound(
-                        "No active role found for user".to_string(),
-                    )
-                    .into(),
-                )
-        }
-    }
-
     pub async fn get_or_create_key_store(&self, state: &SessionState) -> UserResult<UserKeyStore> {
         let master_key = state.store.get_master_key();
         let key_manager_state = &state.into();
diff --git a/crates/router/src/types/domain/user/decision_manager.rs b/crates/router/src/types/domain/user/decision_manager.rs
index 4206b16df6d..af9918af673 100644
--- a/crates/router/src/types/domain/user/decision_manager.rs
+++ b/crates/router/src/types/domain/user/decision_manager.rs
@@ -1,11 +1,15 @@
 use common_enums::TokenPurpose;
-use diesel_models::{enums::UserStatus, user_role::UserRole};
+use diesel_models::{
+    enums::{UserRoleVersion, UserStatus},
+    user_role::UserRole,
+};
 use error_stack::{report, ResultExt};
 use masking::Secret;
 
 use super::UserFromStorage;
 use crate::{
-    core::errors::{StorageErrorExt, UserErrors, UserResult},
+    core::errors::{UserErrors, UserResult},
+    db::user_role::ListUserRolesByUserIdPayload,
     routes::SessionState,
     services::authentication as auth,
     utils,
@@ -284,11 +288,22 @@ impl NextFlow {
                 {
                     self.user.get_verification_days_left(state)?;
                 }
-                let user_role = self
-                    .user
-                    .get_preferred_or_active_user_role_from_db(state)
+                let user_role = state
+                    .store
+                    .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                        user_id: self.user.get_user_id(),
+                        org_id: None,
+                        merchant_id: None,
+                        profile_id: None,
+                        entity_id: None,
+                        version: Some(UserRoleVersion::V1),
+                        status: Some(UserStatus::Active),
+                        limit: Some(1),
+                    })
                     .await
-                    .to_not_found_response(UserErrors::InternalServerError)?;
+                    .change_context(UserErrors::InternalServerError)?
+                    .pop()
+                    .ok_or(UserErrors::InternalServerError)?;
                 utils::user_role::set_role_permissions_in_cache_by_user_role(state, &user_role)
                     .await;
 
diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs
index dbe3ed6d585..aeb866d8d03 100644
--- a/crates/router/src/utils/user_role.rs
+++ b/crates/router/src/utils/user_role.rs
@@ -4,7 +4,7 @@ use api_models::user_role as user_role_api;
 use common_enums::{EntityType, PermissionGroup};
 use common_utils::id_type;
 use diesel_models::{
-    enums::UserRoleVersion,
+    enums::{UserRoleVersion, UserStatus},
     user_role::{UserRole, UserRoleUpdate},
 };
 use error_stack::{report, Report, ResultExt};
@@ -14,6 +14,7 @@ use storage_impl::errors::StorageError;
 use crate::{
     consts,
     core::errors::{UserErrors, UserResult},
+    db::user_role::ListUserRolesByUserIdPayload,
     routes::SessionState,
     services::authorization::{self as authz, permissions::Permission, roles},
     types::domain,
@@ -247,3 +248,113 @@ pub async fn get_single_merchant_id(
             .attach_printable("merchant_id not found"),
     }
 }
+
+pub async fn get_lineage_for_user_id_and_entity_for_accepting_invite(
+    state: &SessionState,
+    user_id: &str,
+    entity_id: String,
+    entity_type: EntityType,
+) -> UserResult<
+    Option<(
+        id_type::OrganizationId,
+        id_type::MerchantId,
+        Option<id_type::ProfileId>,
+    )>,
+> {
+    match entity_type {
+        EntityType::Internal | EntityType::Organization => {
+            Err(UserErrors::InvalidRoleOperation.into())
+        }
+        EntityType::Merchant => {
+            let Ok(merchant_id) = id_type::MerchantId::wrap(entity_id) else {
+                return Ok(None);
+            };
+
+            let user_roles = state
+                .store
+                .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                    user_id,
+                    org_id: None,
+                    merchant_id: Some(&merchant_id),
+                    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::Merchant {
+                    return Ok(None);
+                }
+
+                return Ok(Some((
+                    user_role.org_id.ok_or(UserErrors::InternalServerError)?,
+                    merchant_id,
+                    None,
+                )));
+            }
+
+            Ok(None)
+        }
+        EntityType::Profile => {
+            let Ok(profile_id) = id_type::ProfileId::try_from(std::borrow::Cow::from(entity_id))
+            else {
+                return Ok(None);
+            };
+
+            let user_roles = state
+                .store
+                .list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
+                    user_id,
+                    org_id: None,
+                    merchant_id: None,
+                    profile_id: Some(&profile_id),
+                    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::Profile {
+                    return Ok(None);
+                }
+
+                return Ok(Some((
+                    user_role.org_id.ok_or(UserErrors::InternalServerError)?,
+                    user_role
+                        .merchant_id
+                        .ok_or(UserErrors::InternalServerError)?,
+                    Some(profile_id),
+                )));
+            }
+
+            Ok(None)
+        }
+    }
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 3aae72d8aa7..34e4dc72036 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -424,10 +424,14 @@ pub enum Flow {
     VerifyEmailRequest,
     /// Update user account details
     UpdateUserAccountDetails,
-    /// Accept user invitation
+    /// Accept user invitation using merchant_ids
     AcceptInvitation,
+    /// Accept user invitation using entities
+    AcceptInvitationsV2,
     /// Select merchant from invitations
     MerchantSelect,
+    /// Accept user invitation using entities before user login
+    AcceptInvitationsPreAuth,
     /// Initiate external authentication for a payment
     PaymentsExternalAuthentication,
     /// Authorize the payment after external 3ds authentication
diff --git a/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql
new file mode 100644
index 00000000000..ab5ef65be8b
--- /dev/null
+++ b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/down.sql
@@ -0,0 +1,2 @@
+-- This file should undo anything in `up.sql`
+ALTER TABLE users ADD COLUMN preferred_merchant_id VARCHAR(64);
diff --git a/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql
new file mode 100644
index 00000000000..84af1f99e0c
--- /dev/null
+++ b/migrations/2024-09-01-094614_remove-preferred-merchant-from-users/up.sql
@@ -0,0 +1,2 @@
+-- Your SQL goes here
+ALTER TABLE users DROP COLUMN preferred_merchant_id;

## 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 -->
- Removed preferred_merchant_id from update user API.
- Invites will work at profile level as well.
- Handle internal users in list APIs
  - Org List for internal users will throw error.
  - Merchant list will list merchants.
  - Profile list will list profiles.
- New API for accept invite and merchant select is created which takes entity as request.
- Email token will now have a entity instead of merchant_id.

### Additional Changes

- [x] 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 #5792 

## 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)?
-->
All the following APIs should work without any issues.
- Invite multiple
- Accept invite from email
- Accept invite
- Merchant select

- List orgs for user will throw the following error
```
{
    "error": {
        "type": "invalid_request",
        "message": "User Role Operation Not Supported",
        "code": "UR_23"
    }
}
```


New APIs
Accept Invite (works with JWT Token)
```
curl --location 'http://localhost:8080/user/user/invite/accept/v2' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '[
    {
        "entity_id": "merchant_id",
        "entity_type": "merchant"
    }
]'
```
Response: 200 OK

Accept Invite (works with Merchant Select SPT)
```
curl --location 'http://localhost:8080/user/user/invite/accept/v2/pre_auth' \
--header 'Content-Type: application/json' \
--header 'Authorization: ••••••' \
--data '[
    {
        "entity_id": "merchant_id",
        "entity_type": "merchant"
    }
]'
```
Response: 
```json
{
    "token": "token",
    "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
