Bug: fix(invite): change status to active after resetting the password for invitee




diff --git a/crates/diesel_models/src/query/user.rs b/crates/diesel_models/src/query/user.rs
index b4d5976ba29..6fb5b79ddc1 100644
--- a/crates/diesel_models/src/query/user.rs
+++ b/crates/diesel_models/src/query/user.rs
@@ -3,7 +3,7 @@ use diesel::{
     associations::HasTable, debug_query, result::Error as DieselError, ExpressionMethods,
     JoinOnDsl, QueryDsl,
 };
-use error_stack::{report, IntoReport};
+use error_stack::IntoReport;
 use router_env::{
     logger,
     tracing::{self, instrument},
@@ -49,19 +49,37 @@ impl User {
     pub async fn update_by_user_id(
         conn: &PgPooledConn,
         user_id: &str,
-        user: UserUpdate,
+        user_update: UserUpdate,
     ) -> StorageResult<Self> {
-        generics::generic_update_with_results::<<Self as HasTable>::Table, _, _, _>(
+        generics::generic_update_with_unique_predicate_get_result::<
+            <Self as HasTable>::Table,
+            _,
+            _,
+            _,
+        >(
             conn,
             users_dsl::user_id.eq(user_id.to_owned()),
-            UserUpdateInternal::from(user),
+            UserUpdateInternal::from(user_update),
         )
-        .await?
-        .first()
-        .cloned()
-        .ok_or_else(|| {
-            report!(errors::DatabaseError::NotFound).attach_printable("Error while updating user")
-        })
+        .await
+    }
+
+    pub async fn update_by_user_email(
+        conn: &PgPooledConn,
+        user_email: &str,
+        user_update: UserUpdate,
+    ) -> StorageResult<Self> {
+        generics::generic_update_with_unique_predicate_get_result::<
+            <Self as HasTable>::Table,
+            _,
+            _,
+            _,
+        >(
+            conn,
+            users_dsl::email.eq(user_email.to_owned()),
+            UserUpdateInternal::from(user_update),
+        )
+        .await
     }
 
     pub async fn delete_by_user_id(conn: &PgPooledConn, user_id: &str) -> StorageResult<bool> {
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index ae66728e140..4f50845c8d4 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -1,4 +1,6 @@
 use api_models::user::{self as user_api, InviteMultipleUserResponse};
+#[cfg(feature = "email")]
+use diesel_models::user_role::UserRoleUpdate;
 use diesel_models::{enums::UserStatus, user as storage_user, user_role::UserRoleNew};
 #[cfg(feature = "email")]
 use error_stack::IntoReport;
@@ -357,18 +359,10 @@ pub async fn reset_password(
 
     let hash_password = utils::user::password::generate_password_hash(password.get_secret())?;
 
-    //TODO: Create Update by email query
-    let user_id = state
-        .store
-        .find_user_by_email(token.get_email())
-        .await
-        .change_context(UserErrors::InternalServerError)?
-        .user_id;
-
-    state
+    let user = state
         .store
-        .update_user_by_user_id(
-            user_id.as_str(),
+        .update_user_by_email(
+            token.get_email(),
             storage_user::UserUpdate::AccountUpdate {
                 name: None,
                 password: Some(hash_password),
@@ -379,7 +373,20 @@ pub async fn reset_password(
         .await
         .change_context(UserErrors::InternalServerError)?;
 
-    //TODO: Update User role status for invited user
+    if let Some(inviter_merchant_id) = token.get_merchant_id() {
+        let update_status_result = state
+            .store
+            .update_user_role_by_user_id_merchant_id(
+                user.user_id.clone().as_str(),
+                inviter_merchant_id,
+                UserRoleUpdate::UpdateStatus {
+                    status: UserStatus::Active,
+                    modified_by: user.user_id,
+                },
+            )
+            .await;
+        logger::info!(?update_status_result);
+    }
 
     Ok(ApplicationResponse::StatusOk)
 }
@@ -462,7 +469,7 @@ pub async fn invite_user(
             .store
             .insert_user_role(UserRoleNew {
                 user_id: new_user.get_user_id().to_owned(),
-                merchant_id: user_from_token.merchant_id,
+                merchant_id: user_from_token.merchant_id.clone(),
                 role_id: request.role_id,
                 org_id: user_from_token.org_id,
                 status: invitation_status,
@@ -488,6 +495,7 @@ pub async fn invite_user(
                 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,
             };
             let send_email_result = state
                 .email_client
@@ -664,6 +672,7 @@ async fn handle_new_user_invitation(
             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(),
         };
         let send_email_result = state
             .email_client
diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs
index e88d59ea9f3..67f4230d878 100644
--- a/crates/router/src/db/kafka_store.rs
+++ b/crates/router/src/db/kafka_store.rs
@@ -1896,6 +1896,16 @@ impl UserInterface for KafkaStore {
             .await
     }
 
+    async fn update_user_by_email(
+        &self,
+        user_email: &str,
+        user: storage::UserUpdate,
+    ) -> CustomResult<storage::User, errors::StorageError> {
+        self.diesel_store
+            .update_user_by_email(user_email, user)
+            .await
+    }
+
     async fn delete_user_by_user_id(
         &self,
         user_id: &str,
diff --git a/crates/router/src/db/user.rs b/crates/router/src/db/user.rs
index ecd71f7e2c9..c7c005a0b52 100644
--- a/crates/router/src/db/user.rs
+++ b/crates/router/src/db/user.rs
@@ -33,6 +33,12 @@ pub trait UserInterface {
         user: storage::UserUpdate,
     ) -> CustomResult<storage::User, errors::StorageError>;
 
+    async fn update_user_by_email(
+        &self,
+        user_email: &str,
+        user: storage::UserUpdate,
+    ) -> CustomResult<storage::User, errors::StorageError>;
+
     async fn delete_user_by_user_id(
         &self,
         user_id: &str,
@@ -92,6 +98,18 @@ impl UserInterface for Store {
             .into_report()
     }
 
+    async fn update_user_by_email(
+        &self,
+        user_email: &str,
+        user: storage::UserUpdate,
+    ) -> CustomResult<storage::User, errors::StorageError> {
+        let conn = connection::pg_connection_write(self).await?;
+        storage::User::update_by_user_email(&conn, user_email, user)
+            .await
+            .map_err(Into::into)
+            .into_report()
+    }
+
     async fn delete_user_by_user_id(
         &self,
         user_id: &str,
@@ -229,6 +247,50 @@ impl UserInterface for MockDb {
             )
     }
 
+    async fn update_user_by_email(
+        &self,
+        user_email: &str,
+        update_user: storage::UserUpdate,
+    ) -> CustomResult<storage::User, errors::StorageError> {
+        let mut users = self.users.lock().await;
+        let user_email_pii: common_utils::pii::Email = user_email
+            .to_string()
+            .try_into()
+            .map_err(|_| errors::StorageError::MockDbError)?;
+        users
+            .iter_mut()
+            .find(|user| user.email == user_email_pii)
+            .map(|user| {
+                *user = match &update_user {
+                    storage::UserUpdate::VerifyUser => storage::User {
+                        is_verified: true,
+                        ..user.to_owned()
+                    },
+                    storage::UserUpdate::AccountUpdate {
+                        name,
+                        password,
+                        is_verified,
+                        preferred_merchant_id,
+                    } => storage::User {
+                        name: name.clone().map(Secret::new).unwrap_or(user.name.clone()),
+                        password: password.clone().unwrap_or(user.password.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()
+                    },
+                };
+                user.to_owned()
+            })
+            .ok_or(
+                errors::StorageError::ValueNotFound(format!(
+                    "No user available for user_email = {user_email}"
+                ))
+                .into(),
+            )
+    }
+
     async fn delete_user_by_user_id(
         &self,
         user_id: &str,
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index d5aa9926130..c68907c2846 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -92,18 +92,21 @@ Email         : {user_email}
 #[derive(serde::Serialize, serde::Deserialize)]
 pub struct EmailToken {
     email: String,
+    merchant_id: Option<String>,
     exp: u64,
 }
 
 impl EmailToken {
     pub async fn new_token(
         email: domain::UserEmail,
+        merchant_id: Option<String>,
         settings: &configs::settings::Settings,
     ) -> CustomResult<String, UserErrors> {
         let expiration_duration = std::time::Duration::from_secs(consts::EMAIL_TOKEN_TIME_IN_SECS);
         let exp = jwt::generate_exp(expiration_duration)?.as_secs();
         let token_payload = Self {
             email: email.get_secret().expose(),
+            merchant_id,
             exp,
         };
         jwt::generate_jwt(&token_payload, settings).await
@@ -112,6 +115,10 @@ impl EmailToken {
     pub fn get_email(&self) -> &str {
         self.email.as_str()
     }
+
+    pub fn get_merchant_id(&self) -> Option<&str> {
+        self.merchant_id.as_deref()
+    }
 }
 
 pub fn get_link_with_token(
@@ -132,7 +139,7 @@ pub struct VerifyEmail {
 #[async_trait::async_trait]
 impl EmailData for VerifyEmail {
     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
-        let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
+        let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
             .await
             .change_context(EmailError::TokenGenerationFailure)?;
 
@@ -161,7 +168,7 @@ pub struct ResetPassword {
 #[async_trait::async_trait]
 impl EmailData for ResetPassword {
     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
-        let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
+        let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
             .await
             .change_context(EmailError::TokenGenerationFailure)?;
 
@@ -191,7 +198,7 @@ pub struct MagicLink {
 #[async_trait::async_trait]
 impl EmailData for MagicLink {
     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
-        let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
+        let token = EmailToken::new_token(self.recipient_email.clone(), None, &self.settings)
             .await
             .change_context(EmailError::TokenGenerationFailure)?;
 
@@ -216,14 +223,19 @@ pub struct InviteUser {
     pub user_name: domain::UserName,
     pub settings: std::sync::Arc<configs::settings::Settings>,
     pub subject: &'static str,
+    pub merchant_id: String,
 }
 
 #[async_trait::async_trait]
 impl EmailData for InviteUser {
     async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
-        let token = EmailToken::new_token(self.recipient_email.clone(), &self.settings)
-            .await
-            .change_context(EmailError::TokenGenerationFailure)?;
+        let token = EmailToken::new_token(
+            self.recipient_email.clone(),
+            Some(self.merchant_id.clone()),
+            &self.settings,
+        )
+        .await
+        .change_context(EmailError::TokenGenerationFailure)?;
 
         let invite_user_link =
             get_link_with_token(&self.settings.email.base_url, token, "set_password");

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

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

## Description
<!-- Describe your changes in detail -->
This PR will add an optional `merchant_id` field in `EmailToken` struct. And also changes the status of the user from `invitation_sent` to `active` if the `EmailToken` has `merchant_id`.

### 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).
-->
To change the status of the user to active once the user resets the password.

## 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)?
-->
Postman.
1. Invite a user using this curl
	```
	curl --location 'http://localhost:8080/user/user/invite' \
	--header 'Content-Type: application/json' \
	--header 'Authorization: Bearer JWT' \
	--data-raw '{
	    "email": "new email",
	    "name": "user name",
	    "role_id": "merchant_admin"
	}'
	```
	You should get invite email to the email you have provided and this following response from the api.
	```
	{
	    "is_email_sent": true,
	    "password": null
	}
	```

2. Use the link in the email to hit the reset password api from the dashboard or the following curl can be used to hit the api if email token is extracted.
	```
	curl --location 'http://localhost:8080/user/reset_password' \
	--header 'Content-Type: application/json' \
	--data-raw '{
	    "token": "Email Token",
	    "password": "new password"
	}'
	```
	You will get 200 OK and the status of the `user_role` with the corresponding `user_id` and `merchant_id` will be active.
## Checklist
<!-- Put an `x` in the boxes that apply -->

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