Bug: ProdIntent Changes



ProdIntent changes for providing context of product_type and also converting it into merchant-scope from user scope.
diff --git a/crates/api_models/src/user/dashboard_metadata.rs b/crates/api_models/src/user/dashboard_metadata.rs
index fa550d671fb..8e734d31b53 100644
--- a/crates/api_models/src/user/dashboard_metadata.rs
+++ b/crates/api_models/src/user/dashboard_metadata.rs
@@ -1,4 +1,4 @@
-use common_enums::CountryAlpha2;
+use common_enums::{CountryAlpha2, MerchantProductType};
 use common_utils::{id_type, pii};
 use masking::Secret;
 use strum::EnumString;
@@ -103,6 +103,8 @@ pub struct ProdIntent {
     pub poc_contact: Option<String>,
     pub comments: Option<String>,
     pub is_completed: bool,
+    #[serde(default)]
+    pub product_type: MerchantProductType,
 }
 
 #[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
diff --git a/crates/common_enums/src/enums/accounts.rs b/crates/common_enums/src/enums/accounts.rs
index d9c83312d6b..e5b3ef733c6 100644
--- a/crates/common_enums/src/enums/accounts.rs
+++ b/crates/common_enums/src/enums/accounts.rs
@@ -1,6 +1,7 @@
 use serde;
 use utoipa::ToSchema;
 #[derive(
+    Copy,
     Default,
     Clone,
     Debug,
diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs
index b9ebdf13028..91b20621da8 100644
--- a/crates/router/src/core/user.rs
+++ b/crates/router/src/core/user.rs
@@ -2974,7 +2974,7 @@ pub async fn list_merchants_for_user_in_org(
             .map(|merchant_account| user_api::UserMerchantAccountResponse {
                 merchant_name: merchant_account.merchant_name.clone(),
                 merchant_id: merchant_account.get_id().to_owned(),
-                product_type: merchant_account.product_type.clone(),
+                product_type: merchant_account.product_type,
                 version: merchant_account.version,
             })
             .collect::<Vec<_>>(),
diff --git a/crates/router/src/core/user/dashboard_metadata.rs b/crates/router/src/core/user/dashboard_metadata.rs
index d79d155cba9..a4068fc4fad 100644
--- a/crates/router/src/core/user/dashboard_metadata.rs
+++ b/crates/router/src/core/user/dashboard_metadata.rs
@@ -38,7 +38,6 @@ pub async fn set_metadata(
     Ok(ApplicationResponse::StatusOk)
 }
 
-#[cfg(feature = "v1")]
 pub async fn get_multiple_metadata(
     state: SessionState,
     user: UserFromToken,
@@ -462,7 +461,7 @@ async fn insert_metadata(
                 pii::Email::from_str(inner_poc_email)
                     .change_context(UserErrors::EmailParsingError)?;
             }
-            let mut metadata = utils::insert_user_scoped_metadata_to_db(
+            let mut metadata = utils::insert_merchant_scoped_metadata_to_db(
                 state,
                 user.user_id.clone(),
                 user.merchant_id.clone(),
@@ -473,7 +472,7 @@ async fn insert_metadata(
             .await;
 
             if utils::is_update_required(&metadata) {
-                metadata = utils::update_user_scoped_metadata(
+                metadata = utils::update_merchant_scoped_metadata(
                     state,
                     user.user_id.clone(),
                     user.merchant_id.clone(),
@@ -500,7 +499,6 @@ async fn insert_metadata(
                         EntityType::Merchant,
                     )
                     .await?;
-
                     let email_contents = email_types::BizEmailProd::new(
                         state,
                         data,
@@ -662,7 +660,6 @@ async fn fetch_metadata(
     Ok(dashboard_metadata)
 }
 
-#[cfg(feature = "v1")]
 pub async fn backfill_metadata(
     state: &SessionState,
     user: &UserFromToken,
@@ -705,6 +702,11 @@ pub async fn backfill_metadata(
                 return Ok(None);
             };
 
+            #[cfg(feature = "v1")]
+            let processor_name = mca.connector_name.clone();
+
+            #[cfg(feature = "v2")]
+            let processor_name = mca.connector_name.to_string().clone();
             Some(
                 insert_metadata(
                     state,
@@ -712,13 +714,14 @@ pub async fn backfill_metadata(
                     DBEnum::StripeConnected,
                     types::MetaData::StripeConnected(api::ProcessorConnected {
                         processor_id: mca.get_id(),
-                        processor_name: mca.connector_name,
+                        processor_name,
                     }),
                 )
                 .await,
             )
             .transpose()
         }
+
         DBEnum::PaypalConnected => {
             let mca = if let Some(paypal_connected) = get_merchant_connector_account_by_name(
                 state,
@@ -745,6 +748,11 @@ pub async fn backfill_metadata(
                 return Ok(None);
             };
 
+            #[cfg(feature = "v1")]
+            let processor_name = mca.connector_name.clone();
+
+            #[cfg(feature = "v2")]
+            let processor_name = mca.connector_name.to_string().clone();
             Some(
                 insert_metadata(
                     state,
@@ -752,7 +760,7 @@ pub async fn backfill_metadata(
                     DBEnum::PaypalConnected,
                     types::MetaData::PaypalConnected(api::ProcessorConnected {
                         processor_id: mca.get_id(),
-                        processor_name: mca.connector_name,
+                        processor_name,
                     }),
                 )
                 .await,
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index 2200567ddf9..1879d19de04 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -2136,6 +2136,12 @@ impl User {
                 ),
         );
 
+        route = route.service(
+            web::resource("/data")
+                .route(web::get().to(user::get_multiple_dashboard_metadata))
+                .route(web::post().to(user::set_dashboard_metadata)),
+        );
+
         route
     }
 }
diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs
index 1af22d0338a..d075048ddb6 100644
--- a/crates/router/src/routes/user.rs
+++ b/crates/router/src/routes/user.rs
@@ -197,7 +197,6 @@ pub async fn set_dashboard_metadata(
     .await
 }
 
-#[cfg(feature = "v1")]
 pub async fn get_multiple_dashboard_metadata(
     state: web::Data<AppState>,
     req: HttpRequest,
diff --git a/crates/router/src/services/email/assets/bizemailprod.html b/crates/router/src/services/email/assets/bizemailprod.html
index 3645c953a02..2c973f2d622 100644
--- a/crates/router/src/services/email/assets/bizemailprod.html
+++ b/crates/router/src/services/email/assets/bizemailprod.html
@@ -104,6 +104,10 @@
                             <strong>Business Website:</strong>
                             {business_website}
                           </li>
+                          <li>
+                            <strong>Product Type:</strong>
+                            {product_type}
+                          </li>
                         </ol>
                       </td>
                     </tr>
diff --git a/crates/router/src/services/email/types.rs b/crates/router/src/services/email/types.rs
index 5f611bc1b3a..d663f72cec7 100644
--- a/crates/router/src/services/email/types.rs
+++ b/crates/router/src/services/email/types.rs
@@ -1,5 +1,5 @@
 use api_models::user::dashboard_metadata::ProdIntent;
-use common_enums::EntityType;
+use common_enums::{EntityType, MerchantProductType};
 use common_utils::{errors::CustomResult, pii, types::theme::EmailThemeConfig};
 use error_stack::ResultExt;
 use external_services::email::{EmailContents, EmailData, EmailError};
@@ -64,6 +64,7 @@ pub enum EmailBody {
         legal_business_name: String,
         business_location: String,
         business_website: String,
+        product_type: MerchantProductType,
     },
     ReconActivation {
         user_name: String,
@@ -199,6 +200,7 @@ pub mod html {
                 legal_business_name,
                 business_location,
                 business_website,
+                product_type,
             } => {
                 format!(
                     include_str!("assets/bizemailprod.html"),
@@ -207,6 +209,7 @@ pub mod html {
                     business_location = business_location,
                     business_website = business_website,
                     username = user_name,
+                    product_type = product_type
                 )
             }
             EmailBody::ProFeatureRequest {
@@ -558,6 +561,7 @@ pub struct BizEmailProd {
     pub settings: std::sync::Arc<configs::Settings>,
     pub theme_id: Option<String>,
     pub theme_config: EmailThemeConfig,
+    pub product_type: MerchantProductType,
 }
 
 impl BizEmailProd {
@@ -582,6 +586,7 @@ impl BizEmailProd {
             business_website: data.business_website.unwrap_or_default(),
             theme_id,
             theme_config,
+            product_type: data.product_type,
         })
     }
 }
@@ -595,6 +600,7 @@ impl EmailData for BizEmailProd {
             legal_business_name: self.legal_business_name.clone(),
             business_location: self.business_location.clone(),
             business_website: self.business_website.clone(),
+            product_type: self.product_type,
         });
 
         Ok(EmailContents {
diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs
index 4854d1b4786..8bf177035ef 100644
--- a/crates/router/src/types/domain/user.rs
+++ b/crates/router/src/types/domain/user.rs
@@ -417,7 +417,7 @@ impl NewUserMerchant {
     }
 
     pub fn get_product_type(&self) -> Option<common_enums::MerchantProductType> {
-        self.product_type.clone()
+        self.product_type
     }
 
     pub async fn check_if_already_exists_in_db(&self, state: SessionState) -> UserResult<()> {
@@ -703,11 +703,18 @@ impl TryFrom<UserMerchantCreateRequestWithToken> for NewUserMerchant {
         } else {
             id_type::MerchantId::new_from_unix_timestamp()
         };
+        let (user_from_storage, user_merchant_create, user_from_token) = value;
         Ok(Self {
             merchant_id,
-            company_name: Some(UserCompanyName::new(value.1.company_name.clone())?),
-            product_type: value.1.product_type.clone(),
-            new_organization: NewUserOrganization::from(value),
+            company_name: Some(UserCompanyName::new(
+                user_merchant_create.company_name.clone(),
+            )?),
+            product_type: user_merchant_create.product_type,
+            new_organization: NewUserOrganization::from((
+                user_from_storage,
+                user_merchant_create,
+                user_from_token,
+            )),
         })
     }
 }
diff --git a/crates/router/src/utils/user/dashboard_metadata.rs b/crates/router/src/utils/user/dashboard_metadata.rs
index c19b52b6f86..a91a3fe917b 100644
--- a/crates/router/src/utils/user/dashboard_metadata.rs
+++ b/crates/router/src/utils/user/dashboard_metadata.rs
@@ -218,10 +218,9 @@ pub fn separate_metadata_type_based_on_scope(
             | DBEnum::SetupWoocomWebhook
             | DBEnum::OnboardingSurvey
             | DBEnum::IsMultipleConfiguration
-            | DBEnum::ReconStatus => merchant_scoped.push(key),
-            DBEnum::Feedback | DBEnum::ProdIntent | DBEnum::IsChangePasswordRequired => {
-                user_scoped.push(key)
-            }
+            | DBEnum::ReconStatus
+            | DBEnum::ProdIntent => merchant_scoped.push(key),
+            DBEnum::Feedback | DBEnum::IsChangePasswordRequired => user_scoped.push(key),
         }
     }
     (merchant_scoped, user_scoped)

## 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 introduces support for associating ProdIntent with specific product_types and transitions the feature from being user-scoped to merchant-scoped, enabling more flexible usage across different products.

##  Changes Introduced

### 1. New V2 Routes for Dashboard Metadata
- Added v2 routes to support `ProdIntent` creation and retrieval with respect to `product_type`.

### 2. Product-Type Context for ProdIntent
- While inserting the `ProdIntent` into the `dashboard_metadata` table (for both **v1** and **v2** schemas), the `product_type` is now included in the metadata.

### 3. Merchant-Scoped ProdIntent
- **Before:** `ProdIntent` was **user-scoped**, allowing a single request per user.
- **Now:** It is **merchant-scoped**, allowing a user to raise separate `ProdIntent` requests for different merchant accounts for each product by the same user.

### 4. Enhanced Email Body
- The email body sent for `ProdIntent` now includes the `product_type` context, helping understand for which product the prod intent was raised.
### 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).
-->
Enable requesting for Production Access for every product type merchant account.

## 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)?
-->
Hit the curl:
-  Dashboard Metadata - ProdIntent (POST):

```bash
curl --location 'http://localhost:8080/user/data' \
--header 'accept: */*' \
--header 'accept-language: en-US,en;q=0.9' \
--header 'api-key: hyperswitch' \
--header 'content-type: application/json' \
--header 'origin: https://app.hyperswitch.io' \
--header 'priority: u=1, i' \
--header 'referer: https://app.hyperswitch.io/dashboard/home' \
--header 'sec-ch-ua: "Google Chrome";v="131", "Chromium";v="131", "Not_A Brand";v="24"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"' \
--header 'sec-fetch-dest: empty' \
--header 'sec-fetch-mode: cors' \
--header 'sec-fetch-site: same-origin' \
--header 'user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36' \
--header 'x-merchant-id: merchant_1715600622' \
--header 'x-profile-id: pro_CqAdpUQBUUam1n0g56mB' \
--header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiOWExMGExMTYtZDc1Ny00MjBlLTkzNWQtZWRlZmQ5ZmQ3NTFmIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzQzMDUxMDM3Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTc0MzIyMzg0Mywib3JnX2lkIjoib3JnX1h5ZmR4eUttS1RGTEtuNTJ1clBVIiwicHJvZmlsZV9pZCI6InByb190Y3NsZWJDNDhmN1pkNG9mVlRqWCIsInRlbmFudF9pZCI6InB1YmxpYyJ9.NuvkkzrYPcba0JKUUrlM39NfyD2rHZ_jHIkHgPyM39s' \
--header 'Cookie: login_token=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMzcwZWUwYWUtZDI0ZS00ODJhLTlmNDktZDYzYmZkNGE0ZjVhIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF9qdmMzb1dXRmxkb0xHNG1GbFc0UCIsInJvbGVfaWQiOiJvcmdfYWRtaW4iLCJleHAiOjE3NDMxMDQxMzcsIm9yZ19pZCI6Im9yZ19hU3NCOWozTWVJcWxVS0YwbDJxRCIsInByb2ZpbGVfaWQiOiJwcm9fcGdrWUkzcnl6TlU2NlBLa3A4ZjciLCJ0ZW5hbnRfaWQiOiJwdWJsaWMifQ.oRERTxTsgrZWNXwSJ5w8CvDHOW1qB2GV3wBQ5Mv-BC8' \
--data-raw '{
    "ProdIntent": {
        "poc_email": "berry@gmail.com",
        "is_completed": true,
        "legal_business_name": "blueberry",
        "business_location": "IN",
        "business_website": "https://google.com",
        "poc_name": "berry",
        "comments": "tx123",
        "product_type": "orchestration"
    }
}'
```

Dashboard Metadata table should get inserted/updated with the record along with the product_type in the json body.
Sample entry to table:
<img width="1211" alt="image" src="https://github.com/user-attachments/assets/3312b990-6fcf-4a13-af95-cb63d4e5bb4c" />

Also, product_type should also be sent in the email body to the configured recepient email-id.
![WhatsApp Image 2025-03-28 at 12 58 24 AM](https://github.com/user-attachments/assets/e6082260-70f6-43f0-b71a-7e1b83b50da5)


-  Dashboard Metadata - ProdIntent (GET):

```bash
curl --location 'http://localhost:8080/user/data?keys=ProdIntent' \
--header 'X-Merchant-Id: ddcd_oBuAVsHl0XMLag9JsHCV' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiM2RlMDFhYWYtOWVkMi00YTY5LTg3YWItNWU1YmI2ZWFiNGQ5IiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzQyMjEyMzQxIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTc0Mjk4MDgyOCwib3JnX2lkIjoib3JnX3pMQmRNTnpLaDZrNmhiT2tockdJIiwicHJvZmlsZV9pZCI6InByb19LWnhHZ21rUHFkMldQdXBFa2psWCIsInRlbmFudF9pZCI6InB1YmxpYyJ9.aD185AZEOcIjHnLnZZ9GJ2ujSHIdRPbPtrYhzNo0V4M' \
--header 'sec-ch-ua-platform: "macOS"' \
--header 'Referer: https://integ.hyperswitch.io/dashboard/v2/recon' \
--header 'sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"' \
--header 'X-Profile-Id: pro_UaxaxwTw4A0a9D0QNBrf' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'api-key: hyperswitch' \
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' \
--header 'Content-Type: application/json'
```
Expected Output:
```json
[
    {
        "ProdIntent": {
            "legal_business_name": "blueberry",
            "business_label": null,
            "business_location": "IN",
            "display_name": null,
            "poc_email": "berry@gmail.com",
            "business_type": null,
            "business_identifier": null,
            "business_website": "https://google.com",
            "poc_name": "berry",
            "poc_contact": null,
            "comments": "tx123",
            "is_completed": true,
            "product_type": "orchestration"
        }
    }
]
```
Similarly v2 routes:
`http://localhost:8080/v2/user/data` (POST)
`http://localhost:8080/v2/user/data?keys=ProdIntent` (GET)



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