Bug: [FEATURE] L2 L3 data support for nuvei



### Feature Description

Add support for L2 & L3 Data support for nuvei

### Possible Implementation

-

### Have you spent some time checking if this feature request has been raised before?

- [x] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

None
diff --git a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs
index 355e6f0c4ff..0e032c3c804 100644
--- a/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/nuvei/transformers.rs
@@ -19,7 +19,7 @@ use hyperswitch_domain_models::{
     },
     router_data::{
         AdditionalPaymentMethodConnectorResponse, ConnectorAuthType, ConnectorResponseData,
-        ErrorResponse, RouterData,
+        ErrorResponse, L2L3Data, RouterData,
     },
     router_flow_types::{
         refunds::{Execute, RSync},
@@ -94,9 +94,6 @@ trait NuveiAuthorizePreprocessingCommon {
         &self,
     ) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>>;
     fn get_is_partial_approval(&self) -> Option<PartialApprovalFlag>;
-    fn get_order_tax_amount(
-        &self,
-    ) -> Result<Option<MinorUnit>, error_stack::Report<errors::ConnectorError>>;
     fn is_customer_initiated_mandate_payment(&self) -> bool;
 }
 
@@ -158,11 +155,6 @@ impl NuveiAuthorizePreprocessingCommon for SetupMandateRequestData {
     ) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>> {
         Ok(self.payment_method_data.clone())
     }
-    fn get_order_tax_amount(
-        &self,
-    ) -> Result<Option<MinorUnit>, error_stack::Report<errors::ConnectorError>> {
-        Ok(None)
-    }
 
     fn get_minor_amount_required(
         &self,
@@ -243,11 +235,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsAuthorizeData {
     ) -> Result<PaymentMethodData, error_stack::Report<errors::ConnectorError>> {
         Ok(self.payment_method_data.clone())
     }
-    fn get_order_tax_amount(
-        &self,
-    ) -> Result<Option<MinorUnit>, error_stack::Report<errors::ConnectorError>> {
-        Ok(self.order_tax_amount)
-    }
 
     fn get_email_required(&self) -> Result<Email, error_stack::Report<errors::ConnectorError>> {
         self.get_email()
@@ -325,11 +312,6 @@ impl NuveiAuthorizePreprocessingCommon for PaymentsPreProcessingData {
             .into(),
         )
     }
-    fn get_order_tax_amount(
-        &self,
-    ) -> Result<Option<MinorUnit>, error_stack::Report<errors::ConnectorError>> {
-        Ok(None)
-    }
 
     fn get_is_partial_approval(&self) -> Option<PartialApprovalFlag> {
         None
@@ -376,8 +358,62 @@ pub struct NuveiSessionResponse {
 
 #[derive(Debug, Serialize, Default)]
 #[serde(rename_all = "camelCase")]
-pub struct NuvieAmountDetails {
-    total_tax: Option<StringMajorUnit>,
+pub struct NuveiAmountDetails {
+    pub total_tax: Option<StringMajorUnit>,
+    pub total_shipping: Option<StringMajorUnit>,
+    pub total_handling: Option<StringMajorUnit>,
+    pub total_discount: Option<StringMajorUnit>,
+}
+
+#[derive(Debug, Serialize, Deserialize, Clone, Default)]
+#[serde(rename_all = "snake_case")]
+pub enum NuveiItemType {
+    #[default]
+    Physical,
+    Discount,
+    #[serde(rename = "Shipping_fee")]
+    ShippingFee,
+    Digital,
+    #[serde(rename = "Gift_card")]
+    GiftCard,
+    #[serde(rename = "Store_credit")]
+    StoreCredit,
+    Surcharge,
+    #[serde(rename = "Sales_tax")]
+    SalesTax,
+}
+impl From<Option<enums::ProductType>> for NuveiItemType {
+    fn from(value: Option<enums::ProductType>) -> Self {
+        match value {
+            Some(enums::ProductType::Digital) => Self::Digital,
+            Some(enums::ProductType::Physical) => Self::Physical,
+            Some(enums::ProductType::Ride)
+            | Some(enums::ProductType::Travel)
+            | Some(enums::ProductType::Accommodation) => Self::ShippingFee,
+            _ => Self::Physical,
+        }
+    }
+}
+
+#[serde_with::skip_serializing_none]
+#[derive(Debug, Serialize, Default)]
+#[serde(rename_all = "camelCase")]
+pub struct NuveiItem {
+    pub name: String,
+    #[serde(rename = "type")]
+    pub item_type: NuveiItemType,
+    pub price: StringMajorUnit,
+    pub quantity: String,
+    pub group_id: Option<String>,
+    pub discount: Option<StringMajorUnit>,
+    pub discount_rate: Option<String>,
+    pub shipping: Option<StringMajorUnit>,
+    pub shipping_tax: Option<StringMajorUnit>,
+    pub shipping_tax_rate: Option<String>,
+    pub tax: Option<StringMajorUnit>,
+    pub tax_rate: Option<String>,
+    pub image_url: Option<String>,
+    pub product_url: Option<String>,
 }
 #[serde_with::skip_serializing_none]
 #[derive(Debug, Serialize, Default)]
@@ -404,7 +440,8 @@ pub struct NuveiPaymentsRequest {
     pub shipping_address: Option<ShippingAddress>,
     pub related_transaction_id: Option<String>,
     pub url_details: Option<UrlDetails>,
-    pub amount_details: Option<NuvieAmountDetails>,
+    pub amount_details: Option<NuveiAmountDetails>,
+    pub items: Option<Vec<NuveiItem>>,
     pub is_partial_approval: Option<PartialApprovalFlag>,
     pub external_scheme_details: Option<ExternalSchemeDetails>,
 }
@@ -1424,6 +1461,94 @@ where
         ..Default::default()
     })
 }
+fn get_l2_l3_items(
+    l2_l3_data: &Option<L2L3Data>,
+    currency: enums::Currency,
+) -> Result<Option<Vec<NuveiItem>>, error_stack::Report<errors::ConnectorError>> {
+    l2_l3_data.as_ref().map_or(Ok(None), |data| {
+        data.order_details
+            .as_ref()
+            .map_or(Ok(None), |order_details_list| {
+                // Map each order to a Result<NuveiItem>
+                let results: Vec<Result<NuveiItem, error_stack::Report<errors::ConnectorError>>> =
+                    order_details_list
+                        .iter()
+                        .map(|order| {
+                            let discount = order
+                                .unit_discount_amount
+                                .map(|amount| {
+                                    convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency)
+                                })
+                                .transpose()?;
+                            let tax = order
+                                .total_tax_amount
+                                .map(|amount| {
+                                    convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency)
+                                })
+                                .transpose()?;
+                            Ok(NuveiItem {
+                                name: order.product_name.clone(),
+                                item_type: order.product_type.clone().into(),
+                                price: convert_amount(
+                                    NUVEI_AMOUNT_CONVERTOR,
+                                    order.amount,
+                                    currency,
+                                )?,
+                                quantity: order.quantity.to_string(),
+                                group_id: order.product_id.clone(),
+                                discount,
+                                discount_rate: None,
+                                shipping: None,
+                                shipping_tax: None,
+                                shipping_tax_rate: None,
+                                tax,
+                                tax_rate: order.tax_rate.map(|rate| rate.to_string()),
+                                image_url: order.product_img_link.clone(),
+                                product_url: None,
+                            })
+                        })
+                        .collect();
+                let mut items = Vec::with_capacity(results.len());
+                for result in results {
+                    match result {
+                        Ok(item) => items.push(item),
+                        Err(err) => return Err(err),
+                    }
+                }
+                Ok(Some(items))
+            })
+    })
+}
+
+fn get_amount_details(
+    l2_l3_data: &Option<L2L3Data>,
+    currency: enums::Currency,
+) -> Result<Option<NuveiAmountDetails>, error_stack::Report<errors::ConnectorError>> {
+    l2_l3_data.as_ref().map_or(Ok(None), |data| {
+        let total_tax = data
+            .order_tax_amount
+            .map(|amount| convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency))
+            .transpose()?;
+        let total_shipping = data
+            .shipping_cost
+            .map(|amount| convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency))
+            .transpose()?;
+        let total_discount = data
+            .discount_amount
+            .map(|amount| convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency))
+            .transpose()?;
+        let total_handling = data
+            .duty_amount
+            .map(|amount| convert_amount(NUVEI_AMOUNT_CONVERTOR, amount, currency))
+            .transpose()?;
+        Ok(Some(NuveiAmountDetails {
+            total_tax,
+            total_shipping,
+            total_handling,
+            total_discount,
+        }))
+    })
+}
 
 impl<F, Req> TryFrom<(&RouterData<F, Req, PaymentsResponseData>, String)> for NuveiPaymentsRequest
 where
@@ -1578,12 +1703,8 @@ where
         })?;
         let return_url = item.request.get_return_url_required()?;
 
-        let amount_details = match item.request.get_order_tax_amount()? {
-            Some(tax) => Some(NuvieAmountDetails {
-                total_tax: Some(convert_amount(NUVEI_AMOUNT_CONVERTOR, tax, currency)?),
-            }),
-            None => None,
-        };
+        let amount_details = get_amount_details(&item.l2_l3_data, currency)?;
+        let l2_l3_items: Option<Vec<NuveiItem>> = get_l2_l3_items(&item.l2_l3_data, currency)?;
         let address = {
             if let Some(billing_address) = item.get_optional_billing() {
                 let mut billing_address = billing_address.clone();
@@ -1631,6 +1752,7 @@ where
                 pending_url: return_url.clone(),
             }),
             amount_details,
+            items: l2_l3_items,
             is_partial_approval: item.request.get_is_partial_approval(),
             ..request
         })
@@ -3011,9 +3133,9 @@ fn convert_to_additional_payment_method_connector_response(
         merchant_advice_code.and_then(|code| get_merchant_advice_code_description(code));
 
     let payment_checks = serde_json::json!({
-        "avs_result_code": avs_code,
+        "avs_result": avs_code,
         "avs_description": avs_description,
-        "cvv_2_reply_code": cvv2_code,
+        "cvv_2_reply": cvv2_code,
         "cvv_2_description": cvv_description,
         "merchant_advice_code": merchant_advice_code,
         "merchant_advice_code_description": merchant_advice_description

## 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 -->
Include support for L2L3Data for nuvei connector

- L2L3 data is primarily used for B2B, government, and corporate card transactions where detailed purchase information is required for lower interchange rates and compliance purposes. 
- `NOTE` : In Nuvei shipping details can be passed for each item but we are not supporting it as of now as it will be included if required practically. Other connectors doesn't support this

<details>
<summary> Payment test </summary>

# Request

```json
{
    "amount": 4324,
    "currency": "EUR",
    "confirm": true,
    "setup_future_usage": "on_session",
    "customer_acceptance": {
        "acceptance_type": "offline",
        "accepted_at": "1963-05-03T04:07:52.723Z",
        "online": {
            "ip_address": "127.0.0.1",
            "user_agent": "amet irure esse"
        }
    },
    "connector": [
        "nuvei"
    ],
    "customer_id": "nidthxxinn",
    "return_url": "https://www.google.com",
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_type": "credit",
    "authentication_type": "no_three_ds",
    "description": "hellow world",
    "billing": {
        "address": {
            "zip": "560095",
            "country": "US",
            "first_name": "Sakil",
            "last_name": "Mostak",
            "line1": "Fasdf",
            "line2": "Fasdf",
            "city": "Fasdf"
        }
    },
    "browser_info": {
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "ip_address": "192.168.1.1",
        "java_enabled": false,
        "java_script_enabled": true,
        "language": "en-US",
        "color_depth": 24,
        "screen_height": 1080,
        "screen_width": 1920,
        "time_zone": 330,
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
    },
    "email": "hello@gmail.com",
    "payment_method_data": {
        "card": {
            "card_number": "4000000000009995",
            "card_exp_month": "01",
            "card_exp_year": "2026",
            "card_holder_name": "John Smith",
            "card_cvc": "100"
        }
    },
// l2 l3 data 's 
    "order_tax_amount": 8900,
    "shipping_cost": 8299,
    "discount_amount": 3000,
    "duty_amount": 2409,
    "order_details": [ //l2 l3data
        {
            "product_name": "Laptop",
            "quantity": 1,
            "amount": 8000,
            "description": "Business laptop 23214324",
            "sku": "LAP-001",
            "upc": "123456789012",
            "commodity_code": "8471",
            "unit_of_measure": "EA",
            "unit_price": 8000,
            "unit_discount_amount": 200,
            "tax_rate": 0.08,
            "total_tax_amount": 640
        },
        {
            "product_name": "Mouse",
            "quantity": 2,
            "amount": 2000,
            "description": "Wireless office mouse",
            "sku": "MOU-001",
            "upc": "123456789013",
            "commodity_code": "8471",
            "unit_of_measure": "EA",
            "unit_price": 1000,
            "unit_discount_amount": 50,
            "tax_rate": 0.08,
            "total_tax_amount": 160
        }
    ]
}
```
# Response

```json 
{
    "payment_id": "pay_t0Izz9tAgCQtylk9Mcbf",
    "merchant_id": "merchant_1756789409",
    "status": "succeeded",
    "amount": 4324,
    "net_amount": 12623,
    "shipping_cost": 8299,
    "amount_capturable": 0,
    "amount_received": 12623,
    "connector": "nuvei",
    "client_secret": "pay_t0Izz9tAgCQtylk9Mcbf_secret_cjXmBdaE9S6gTQorZcq8",
    "created": "2025-09-07T03:38:00.787Z",
    "currency": "EUR",
    "customer_id": "nidthxxinn",
    "customer": {
        "id": "nidthxxinn",
        "name": null,
        "email": "hello@gmail.com",
        "phone": null,
        "phone_country_code": null
    },
    "description": "hellow world",
    "refunds": null,
    "disputes": null,
    "mandate_id": null,
    "mandate_data": null,
    "setup_future_usage": "on_session",
    "off_session": null,
    "capture_on": null,
    "capture_method": "automatic",
    "payment_method": "card",
    "payment_method_data": {
        "card": {
            "last4": "9995",
            "card_type": "CREDIT",
            "card_network": "Visa",
            "card_issuer": "INTL HDQTRS-CENTER OWNED",
            "card_issuing_country": "UNITEDSTATES",
            "card_isin": "400000",
            "card_extended_bin": null,
            "card_exp_month": "01",
            "card_exp_year": "2026",
            "card_holder_name": "John Smith",
            "payment_checks": {
                "avs_result": "",
                "cvv_2_reply": "",
                "avs_description": null,
                "cvv_2_description": null,
                "merchant_advice_code": "",
                "merchant_advice_code_description": null
            },
            "authentication_data": {
                "challengePreferenceReason": "12"
            }
        },
        "billing": null
    },
    "payment_token": "token_pKckYLN5M2Qcs4MJuGDz",
    "shipping": null,
    "billing": {
        "address": {
            "city": "Fasdf",
            "country": "US",
            "line1": "Fasdf",
            "line2": "Fasdf",
            "line3": null,
            "zip": "560095",
            "state": null,
            "first_name": "Sakil",
            "last_name": "Mostak",
            "origin_zip": null
        },
        "phone": null,
        "email": null
    },
    "order_details": [
        {
            "sku": "LAP-001",
            "upc": "123456789012",
            "brand": null,
            "amount": 8000,
            "category": null,
            "quantity": 1,
            "tax_rate": 0.08,
            "product_id": null,
            "description": "Business laptop 23214324",
            "product_name": "Laptop",
            "product_type": null,
            "sub_category": null,
            "total_amount": null,
            "commodity_code": "8471",
            "unit_of_measure": "EA",
            "product_img_link": null,
            "product_tax_code": null,
            "total_tax_amount": 640,
            "requires_shipping": null,
            "unit_discount_amount": 200
        },
        {
            "sku": "MOU-001",
            "upc": "123456789013",
            "brand": null,
            "amount": 2000,
            "category": null,
            "quantity": 2,
            "tax_rate": 0.08,
            "product_id": null,
            "description": "Wireless office mouse",
            "product_name": "Mouse",
            "product_type": null,
            "sub_category": null,
            "total_amount": null,
            "commodity_code": "8471",
            "unit_of_measure": "EA",
            "product_img_link": null,
            "product_tax_code": null,
            "total_tax_amount": 160,
            "requires_shipping": null,
            "unit_discount_amount": 50
        }
    ],
    "email": "hello@gmail.com",
    "name": null,
    "phone": null,
    "return_url": "https://www.google.com/",
    "authentication_type": "no_three_ds",
    "statement_descriptor_name": null,
    "statement_descriptor_suffix": null,
    "next_action": null,
    "cancellation_reason": null,
    "error_code": null,
    "error_message": null,
    "unified_code": null,
    "unified_message": null,
    "payment_experience": null,
    "payment_method_type": "credit",
    "connector_label": null,
    "business_country": null,
    "business_label": "default",
    "business_sub_label": null,
    "allowed_payment_method_types": null,
    "ephemeral_key": {
        "customer_id": "nidthxxinn",
        "created_at": 1757216280,
        "expires": 1757219880,
        "secret": "epk_e4ed9423c18546afac8b4c6540f4b193"
    },
    "manual_retry_allowed": false,
    "connector_transaction_id": "8110000000013731386",
    "frm_message": null,
    "metadata": null,
    "connector_metadata": null,
    "feature_metadata": {
        "redirect_response": null,
        "search_tags": null,
        "apple_pay_recurring_details": null,
        "gateway_system": "direct"
    },
    "reference_id": "8566044111",
    "payment_link": null,
    "profile_id": "pro_EzO6xGQZdE5pd9I5Rdzs",
    "surcharge_details": null,
    "attempt_count": 1,
    "merchant_decision": null,
    "merchant_connector_id": "mca_i5QVCDvXmBLbZg7n97n2",
    "incremental_authorization_allowed": false,
    "authorization_count": null,
    "incremental_authorizations": null,
    "external_authentication_details": null,
    "external_3ds_authentication_attempted": false,
    "expires_on": "2025-09-07T03:53:00.787Z",
    "fingerprint": null,
    "browser_info": {
        "language": "en-US",
        "time_zone": 330,
        "ip_address": "192.168.1.1",
        "user_agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
        "color_depth": 24,
        "java_enabled": false,
        "screen_width": 1920,
        "accept_header": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
        "screen_height": 1080,
        "java_script_enabled": true
    },
    "payment_channel": null,
    "payment_method_id": null,
    "network_transaction_id": null,
    "payment_method_status": null,
    "updated": "2025-09-07T03:38:04.582Z",
    "split_payments": null,
    "frm_metadata": null,
    "extended_authorization_applied": null,
    "capture_before": null,
    "merchant_order_reference_id": null,
    "order_tax_amount": 8900,
    "connector_mandate_id": null,
    "card_discovery": "manual",
    "force_3ds_challenge": false,
    "force_3ds_challenge_trigger": false,
    "issuer_error_code": null,
    "issuer_error_message": null,
    "is_iframe_redirection_enabled": null,
    "whole_connector_response": null,
    "enable_partial_authorization": null
}
```

</details>






## Checklist
<!-- Put an `x` in the boxes that apply -->

- [ ] I formatted the code `cargo +nightly fmt --all`
- [ ] I addressed lints thrown by `cargo clippy`
- [ ] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
