Bug: feat(invite): Send email to user if user already exist




diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs
index d7150af9bc7..ec3e2dce9b2 100644
--- a/crates/api_models/src/events/user.rs
+++ b/crates/api_models/src/events/user.rs
@@ -10,11 +10,12 @@ use crate::user::{
     dashboard_metadata::{
         GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
     },
-    AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
-    DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse, InviteUserRequest,
-    InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest, SendVerifyEmailRequest,
-    SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest, SwitchMerchantIdRequest,
-    UpdateUserAccountDetailsRequest, UserMerchantCreate, VerifyEmailRequest,
+    AcceptInviteFromEmailRequest, AuthorizeResponse, ChangePasswordRequest, ConnectAccountRequest,
+    CreateInternalUserRequest, DashboardEntryResponse, ForgotPasswordRequest, GetUsersResponse,
+    InviteUserRequest, InviteUserResponse, ReInviteUserRequest, ResetPasswordRequest,
+    SendVerifyEmailRequest, SignInResponse, SignUpRequest, SignUpWithMerchantIdRequest,
+    SwitchMerchantIdRequest, UpdateUserAccountDetailsRequest, UserMerchantCreate,
+    VerifyEmailRequest,
 };
 
 impl ApiEventMetric for DashboardEntryResponse {
@@ -57,6 +58,7 @@ common_utils::impl_misc_api_event_type!(
     ReInviteUserRequest,
     VerifyEmailRequest,
     SendVerifyEmailRequest,
+    AcceptInviteFromEmailRequest,
     SignInResponse,
     UpdateUserAccountDetailsRequest
 );
diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs
index c3e6908c733..8f77f72aad5 100644
--- a/crates/api_models/src/user.rs
+++ b/crates/api_models/src/user.rs
@@ -118,6 +118,11 @@ pub struct ReInviteUserRequest {
     pub email: pii::Email,
 }
 
+#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
+pub struct AcceptInviteFromEmailRequest {
+    pub token: Secret<String>,
+}
+
 #[derive(Debug, serde::Deserialize, serde::Serialize)]
 pub struct SwitchMerchantIdRequest {
     pub merchant_id: String,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index e7639af3918..dfcfe8dbcd4 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -464,7 +464,7 @@ pub async fn invite_user(
             .store
             .insert_user_role(UserRoleNew {
                 user_id: invitee_user_from_db.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: {
@@ -488,8 +488,34 @@ pub async fn invite_user(
                 }
             })?;
 
+        let is_email_sent;
+        #[cfg(feature = "email")]
+        {
+            let email_contents = email_types::InviteRegisteredUser {
+                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,
+            };
+
+            is_email_sent = state
+                .email_client
+                .compose_and_send_email(
+                    Box::new(email_contents),
+                    state.conf.proxy.https_url.as_ref(),
+                )
+                .await
+                .map(|email_result| logger::info!(?email_result))
+                .map_err(|email_result| logger::error!(?email_result))
+                .is_ok();
+        }
+        #[cfg(not(feature = "email"))]
+        {
+            is_email_sent = false;
+        }
         Ok(ApplicationResponse::Json(user_api::InviteUserResponse {
-            is_email_sent: false,
+            is_email_sent,
             password: None,
         }))
     } else if invitee_user
@@ -681,9 +707,37 @@ async fn handle_existing_user_invitation(
             }
         })?;
 
+    let is_email_sent;
+    #[cfg(feature = "email")]
+    {
+        let invitee_email = domain::UserEmail::from_pii_email(request.email.clone())?;
+        let email_contents = email_types::InviteRegisteredUser {
+            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(),
+        };
+
+        is_email_sent = state
+            .email_client
+            .compose_and_send_email(
+                Box::new(email_contents),
+                state.conf.proxy.https_url.as_ref(),
+            )
+            .await
+            .map(|email_result| logger::info!(?email_result))
+            .map_err(|email_result| logger::error!(?email_result))
+            .is_ok();
+    }
+    #[cfg(not(feature = "email"))]
+    {
+        is_email_sent = false;
+    }
+
     Ok(InviteMultipleUserResponse {
         email: request.email.clone(),
-        is_email_sent: false,
+        is_email_sent,
         password: None,
         error: None,
     })
@@ -840,6 +894,67 @@ pub async fn resend_invite(
     Ok(ApplicationResponse::StatusOk)
 }
 
+#[cfg(feature = "email")]
+pub async fn accept_invite_from_email(
+    state: AppState,
+    request: user_api::AcceptInviteFromEmailRequest,
+) -> UserResponse<user_api::DashboardEntryResponse> {
+    let token = request.token.expose();
+
+    let email_token = auth::decode_jwt::<email_types::EmailToken>(&token, &state)
+        .await
+        .change_context(UserErrors::LinkInvalid)?;
+
+    auth::blacklist::check_email_token_in_blacklist(&state, &token).await?;
+
+    let user: domain::UserFromStorage = state
+        .store
+        .find_user_by_email(email_token.get_email())
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let merchant_id = email_token
+        .get_merchant_id()
+        .ok_or(UserErrors::InternalServerError)?;
+
+    let update_status_result = state
+        .store
+        .update_user_role_by_user_id_merchant_id(
+            user.get_user_id(),
+            merchant_id,
+            UserRoleUpdate::UpdateStatus {
+                status: UserStatus::Active,
+                modified_by: user.get_user_id().to_string(),
+            },
+        )
+        .await
+        .change_context(UserErrors::InternalServerError)?;
+
+    let _ = auth::blacklist::insert_email_token_in_blacklist(&state, &token)
+        .await
+        .map_err(|e| logger::error!(?e));
+
+    let user_from_db: domain::UserFromStorage = state
+        .store
+        .update_user_by_user_id(user.get_user_id(), storage_user::UserUpdate::VerifyUser)
+        .await
+        .change_context(UserErrors::InternalServerError)?
+        .into();
+
+    let token =
+        utils::user::generate_jwt_auth_token(&state, &user_from_db, &update_status_result).await?;
+
+    Ok(ApplicationResponse::Json(
+        utils::user::get_dashboard_entry_response(
+            &state,
+            user_from_db,
+            update_status_result,
+            token,
+        )?,
+    ))
+}
+
 pub async fn create_internal_user(
     state: AppState,
     request: user_api::CreateInternalUserRequest,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 41881c60ff7..e9e4b67c782 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -1020,7 +1020,11 @@ impl User {
                     web::resource("/verify_email_request")
                         .route(web::post().to(verify_email_request)),
                 )
-                .service(web::resource("/user/resend_invite").route(web::post().to(resend_invite)));
+                .service(web::resource("/user/resend_invite").route(web::post().to(resend_invite)))
+                .service(
+                    web::resource("/accept_invite_from_email")
+                        .route(web::post().to(accept_invite_from_email)),
+                );
         }
         #[cfg(not(feature = "email"))]
         {
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index 9393e8ae212..efdbb57b33c 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -188,6 +188,7 @@ impl From<Flow> for ApiIdentifier {
             | Flow::UserSignUpWithMerchantId
             | Flow::VerifyEmailWithoutInviteChecks
             | Flow::VerifyEmail
+            | Flow::AcceptInviteFromEmail
             | Flow::VerifyEmailRequest
             | Flow::UpdateUserAccountDetails => Self::User,
 
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index a863bc2b662..dacfe0e59a6 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -420,6 +420,25 @@ pub async fn resend_invite(
     .await
 }
 
+#[cfg(feature = "email")]
+pub async fn accept_invite_from_email(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Json<user_api::AcceptInviteFromEmailRequest>,
+) -> HttpResponse {
+    let flow = Flow::AcceptInviteFromEmail;
+    Box::pin(api::server_wrap(
+        flow,
+        state.clone(),
+        &req,
+        payload.into_inner(),
+        |state, _, request_payload| user_core::accept_invite_from_email(state, request_payload),
+        &auth::NoAuth,
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
 #[cfg(feature = "email")]
 pub async fn verify_email_without_invite_checks(
     state: web::Data<AppState>,
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 598a2620937..46cfad08783 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -28,6 +28,10 @@ pub enum EmailBody {
         link: String,
         user_name: String,
     },
+    AcceptInviteFromEmail {
+        link: String,
+        user_name: String,
+    },
     BizEmailProd {
         user_name: String,
         poc_email: String,
@@ -78,6 +82,14 @@ pub mod html {
                     link = link
                 )
             }
+            // TODO: Change the linked html for accept invite from email
+            EmailBody::AcceptInviteFromEmail { link, user_name } => {
+                format!(
+                    include_str!("assets/invite.html"),
+                    username = user_name,
+                    link = link
+                )
+            }
             EmailBody::ReconActivation { user_name } => {
                 format!(
                     include_str!("assets/recon_activation.html"),
@@ -287,6 +299,42 @@ impl EmailData for InviteUser {
         })
     }
 }
+pub struct InviteRegisteredUser {
+    pub recipient_email: domain::UserEmail,
+    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 InviteRegisteredUser {
+    async fn get_email_data(&self) -> CustomResult<EmailContents, EmailError> {
+        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,
+            "accept_invite_from_email",
+        );
+        let body = html::get_html_body(EmailBody::AcceptInviteFromEmail {
+            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 ReconActivation {
     pub recipient_email: domain::UserEmail,
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 6649b89911a..994b134520e 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -339,6 +339,8 @@ pub enum Flow {
     InviteMultipleUser,
     /// Reinvite user
     ReInviteUser,
+    /// Accept invite from email
+    AcceptInviteFromEmail,
     /// Delete user role
     DeleteUserRole,
     /// Incremental Authorization flow

## 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 -->
This PR will send an email to the user if the user already exist


### 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).
-->
If the user was already exists then we were directly activating the user. With this PR it will send an email to the user and it will redirect it directly to the dashboard

## How did you test it?
1. 
Request: 
``` 
curl --location 'http://localhost:8080/user/user/invite_user \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer JWT of org_admin user' \
--data-raw '{
    "email": "email of the user who already signed up“,
     "name":"some name",
      "role_id":"some valid role"
}
```

Response 
```
{"is_email_sent":true,"password":null}
```

2. Email will be sent to the user email 
3. Copy token from the link
4. 
Request 
```
curl --location 'http://localhost:8080/user/activate_from_email\
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer JWT of org_admin user' \
--data-raw '{
    "token": copied token 
} 
```

Response
```
{
    "token": "JWT",
    "merchant_id": "merchant_id",
    "name": "name from email",
    "email": "invited_email ",
    "verification_days_left": null,
    "user_role": "invited_role"
}
```

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