Bug: Add NTID support for Cybersource



Add `NTID` (network transaction id) flow cybersource.
diff --git a/crates/router/src/connector/cybersource/transformers.rs b/crates/router/src/connector/cybersource/transformers.rs
index 6a51e1e8cd4..346d2127649 100644
--- a/crates/router/src/connector/cybersource/transformers.rs
+++ b/crates/router/src/connector/cybersource/transformers.rs
@@ -297,6 +297,7 @@ pub struct CybersourceAuthorizationOptions {
 #[serde(rename_all = "camelCase")]
 pub struct MerchantInitiatedTransaction {
     reason: Option<String>,
+    previous_transaction_id: Option<Secret<String>>,
     //Required for recurring mandates payment
     original_authorized_amount: Option<String>,
 }
@@ -314,6 +315,7 @@ pub struct CybersourcePaymentInitiator {
 #[serde(rename_all = "camelCase")]
 pub enum CybersourcePaymentInitiatorTypes {
     Customer,
+    Merchant,
 }
 
 #[derive(Debug, Serialize)]
@@ -503,6 +505,24 @@ impl
             Option<String>,
         ),
     ) -> Result<Self, Self::Error> {
+        let mut commerce_indicator = solution
+            .as_ref()
+            .map(|pm_solution| match pm_solution {
+                PaymentSolution::ApplePay => network
+                    .as_ref()
+                    .map(|card_network| match card_network.to_lowercase().as_str() {
+                        "amex" => "aesk",
+                        "discover" => "dipb",
+                        "mastercard" => "spa",
+                        "visa" => "internet",
+                        _ => "internet",
+                    })
+                    .unwrap_or("internet"),
+                PaymentSolution::GooglePay => "internet",
+            })
+            .unwrap_or("internet")
+            .to_string();
+
         let (action_list, action_token_types, authorization_options) = if item
             .router_data
             .request
@@ -531,44 +551,113 @@ impl
                     merchant_intitiated_transaction: None,
                 }),
             )
-        } else if item.router_data.request.connector_mandate_id().is_some() {
-            let original_amount = item
-                .router_data
-                .get_recurring_mandate_payment_data()?
-                .get_original_payment_amount()?;
-            let original_currency = item
+        } else if item.router_data.request.mandate_id.is_some() {
+            match item
                 .router_data
-                .get_recurring_mandate_payment_data()?
-                .get_original_payment_currency()?;
-            (
-                None,
-                None,
-                Some(CybersourceAuthorizationOptions {
-                    initiator: None,
-                    merchant_intitiated_transaction: Some(MerchantInitiatedTransaction {
-                        reason: None,
-                        original_authorized_amount: Some(utils::get_amount_as_string(
-                            &types::api::CurrencyUnit::Base,
-                            original_amount,
-                            original_currency,
-                        )?),
-                    }),
-                }),
-            )
+                .request
+                .mandate_id
+                .clone()
+                .and_then(|mandate_id| mandate_id.mandate_reference_id)
+            {
+                Some(api_models::payments::MandateReferenceId::ConnectorMandateId(_)) => {
+                    let original_amount = item
+                        .router_data
+                        .get_recurring_mandate_payment_data()?
+                        .get_original_payment_amount()?;
+                    let original_currency = item
+                        .router_data
+                        .get_recurring_mandate_payment_data()?
+                        .get_original_payment_currency()?;
+                    (
+                        None,
+                        None,
+                        Some(CybersourceAuthorizationOptions {
+                            initiator: None,
+                            merchant_intitiated_transaction: Some(MerchantInitiatedTransaction {
+                                reason: None,
+                                original_authorized_amount: Some(utils::get_amount_as_string(
+                                    &types::api::CurrencyUnit::Base,
+                                    original_amount,
+                                    original_currency,
+                                )?),
+                                previous_transaction_id: None,
+                            }),
+                        }),
+                    )
+                }
+                Some(api_models::payments::MandateReferenceId::NetworkMandateId(
+                    network_transaction_id,
+                )) => {
+                    let (original_amount, original_currency) = match network
+                        .clone()
+                        .map(|network| network.to_lowercase())
+                        .as_deref()
+                    {
+                        Some("discover") => {
+                            let original_amount = Some(
+                                item.router_data
+                                    .get_recurring_mandate_payment_data()?
+                                    .get_original_payment_amount()?,
+                            );
+                            let original_currency = Some(
+                                item.router_data
+                                    .get_recurring_mandate_payment_data()?
+                                    .get_original_payment_currency()?,
+                            );
+                            (original_amount, original_currency)
+                        }
+                        _ => {
+                            let original_amount = item
+                                .router_data
+                                .recurring_mandate_payment_data
+                                .as_ref()
+                                .and_then(|recurring_mandate_payment_data| {
+                                    recurring_mandate_payment_data
+                                        .original_payment_authorized_amount
+                                });
+
+                            let original_currency = item
+                                .router_data
+                                .recurring_mandate_payment_data
+                                .as_ref()
+                                .and_then(|recurring_mandate_payment_data| {
+                                    recurring_mandate_payment_data
+                                        .original_payment_authorized_currency
+                                });
+
+                            (original_amount, original_currency)
+                        }
+                    };
+
+                    let original_authorized_amount = match (original_amount, original_currency) {
+                        (Some(original_amount), Some(original_currency)) => Some(
+                            utils::to_currency_base_unit(original_amount, original_currency)?,
+                        ),
+                        _ => None,
+                    };
+                    commerce_indicator = "recurring".to_string();
+                    (
+                        None,
+                        None,
+                        Some(CybersourceAuthorizationOptions {
+                            initiator: Some(CybersourcePaymentInitiator {
+                                initiator_type: Some(CybersourcePaymentInitiatorTypes::Merchant),
+                                credential_stored_on_file: None,
+                                stored_credential_used: Some(true),
+                            }),
+                            merchant_intitiated_transaction: Some(MerchantInitiatedTransaction {
+                                reason: Some("7".to_string()),
+                                original_authorized_amount,
+                                previous_transaction_id: Some(Secret::new(network_transaction_id)),
+                            }),
+                        }),
+                    )
+                }
+                None => (None, None, None),
+            }
         } else {
             (None, None, None)
         };
-        let commerce_indicator = match network {
-            Some(card_network) => match card_network.to_lowercase().as_str() {
-                "amex" => "aesk",
-                "discover" => "dipb",
-                "mastercard" => "spa",
-                "visa" => "internet",
-                _ => "internet",
-            },
-            None => "internet",
-        }
-        .to_string();
         Ok(Self {
             capture: Some(matches!(
                 item.router_data.request.capture_method,
@@ -754,11 +843,11 @@ impl
                 expiration_month: ccard.card_exp_month,
                 expiration_year: ccard.card_exp_year,
                 security_code: ccard.card_cvc,
-                card_type,
+                card_type: card_type.clone(),
             },
         });
 
-        let processing_information = ProcessingInformation::try_from((item, None, None))?;
+        let processing_information = ProcessingInformation::try_from((item, None, card_type))?;
         let client_reference_information = ClientReferenceInformation::from(item);
         let merchant_defined_information =
             item.router_data.request.metadata.clone().map(|metadata| {
@@ -1287,6 +1376,7 @@ impl TryFrom<&CybersourceRouterData<&types::PaymentsIncrementalAuthorizationRout
                     }),
                     merchant_intitiated_transaction: Some(MerchantInitiatedTransaction {
                         reason: Some("5".to_owned()),
+                        previous_transaction_id: None,
                         original_authorized_amount: None,
                     }),
                 }),
@@ -1537,6 +1627,7 @@ pub struct ClientReferenceInformation {
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct ClientProcessorInformation {
+    network_transaction_id: Option<String>,
     avs: Option<Avs>,
 }
 
@@ -1667,7 +1758,7 @@ fn get_payment_response(
                     .processor_information
                     .as_ref()
                     .map(|processor_information| serde_json::json!({"avs_response": processor_information.avs})),
-                network_txn_id: None,
+                network_txn_id: info_response.processor_information.as_ref().and_then(|processor_information| processor_information.network_transaction_id.clone()),
                 connector_response_reference_id: Some(
                     info_response
                         .client_reference_information
@@ -2281,6 +2372,7 @@ impl<F>
     }
 }
 
+// zero dollar response
 impl<F, T>
     TryFrom<
         types::ResponseRouterData<
@@ -2328,7 +2420,11 @@ impl<F, T>
                             redirection_data: None,
                             mandate_reference,
                             connector_metadata: None,
-                            network_txn_id: None,
+                            network_txn_id: info_response.processor_information.as_ref().and_then(
+                                |processor_information| {
+                                    processor_information.network_transaction_id.clone()
+                                },
+                            ),
                             connector_response_reference_id: Some(
                                 info_response
                                     .client_reference_information

## 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 -->
Add `NTID` (network transaction id) flow cybersource.

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


## 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)?
-->
enabled `pg_agnotic` flag
```
curl --location 'https://sandbox.hyperswitch.io/routing/business_profile/:business_profile_id/configs/pg_agnostic_mit' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'Authorization: Bearer JWT_Token' \
--data '{
    "enabled": true
}'
```
Created a mandate or CIT in cybersource
<img width="1107" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/3649e1f4-0e19-4c89-b978-e11120a96b92">

```
{
    "amount": 0,
    "currency": "USD",
    "confirm": true,
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "customer_id": "singh",
    "email": "guest@example.com",
    "name": "John Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "authentication_type": "no_three_ds",
    "return_url": "https://google.com",
    "payment_method": "card",
    "payment_method_type": "credit",
    "payment_method_data": {
        "card": {
            "card_number": "4242424242424242",
            "card_exp_month": "03",
            "card_exp_year": "2030",
            "card_holder_name": "joseph Doe",
            "card_cvc": "123"
        }
    },
    "setup_future_usage": "off_session",
    "payment_type": "setup_mandate",
        "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"
            }
    },
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "shipping": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "John",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "browser_info": {
        "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
        "accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "125.0.0.1"
    },
    "statement_descriptor_name": "joseph",
    "statement_descriptor_suffix": "JS",
    "metadata": {},
    "order_details": [
        {
            "product_name": "Apple iphone 15",
            "quantity": 1,
            "amount": 0,
            "account_name": "transaction_processing"
        }
    ]
}
```
DB screenshot where `network_transaction_id` is stored in the `payment_methods` table
![image](https://github.com/juspay/hyperswitch/assets/83439957/f0dee8b8-193f-4013-9ebf-c749dc24643a)
Create a payment with confirm false
<img width="972" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/fce68823-ce21-4353-9908-c19ca4f1f468">
List payment methods for customer
```
curl --location 'http://localhost:8080/customers/:customer_id/payment_methods' \
--header 'Accept: application/json' \
--header 'api-key: api-key'
```
<img width="956" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/c0ea7f9e-3468-41ea-8c13-ee4cba57a293">
Confirm the payment by passing the connector as stripe in routing

```
{
    "amount": 300,
    "currency": "USD",
    "confirm": false,
    "customer_id": "cyb111",
    "return_url": "https://google.com",
    "email": "something@gmail.com",
    "name": "Joseph Doe",
    "phone": "999999999",
    "phone_country_code": "+65",
    "description": "Its my first payment request",
    "statement_descriptor_name": "Juspay",
    "statement_descriptor_suffix": "Router",
    "off_session": true,
    "billing": {
        "address": {
            "line1": "1467",
            "line2": "Harrison Street",
            "line3": "Harrison Street",
            "city": "San Fransico",
            "state": "California",
            "zip": "94122",
            "country": "US",
            "first_name": "joseph",
            "last_name": "Doe"
        },
        "phone": {
            "number": "8056594427",
            "country_code": "+91"
        }
    },
    "connector_metadata": {
        "noon": {
            "order_category": "pay"
        }
    },

    "browser_info": {
        "user_agent": "Mozilla\/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/70.0.3538.110 Safari\/537.36",
        "accept_header": "text\/html,application\/xhtml+xml,application\/xml;q=0.9,image\/webp,image\/apng,*\/*;q=0.8",
        "language": "nl-NL",
        "color_depth": 24,
        "screen_height": 723,
        "screen_width": 1536,
        "time_zone": 0,
        "java_enabled": true,
        "java_script_enabled": true,
        "ip_address": "125.0.0.1"
    }
}
```

```
curl --location 'http://localhost:8080/payments/{{payment_id}}/confirm' \
--header 'api-key: api-key' \
--header 'Content-Type: application/json' \
--data '{
    "payment_token": "payment_token",
    "client_secret": "client_secret",
    "payment_method": "card"
}'
```
<img width="1021" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/d8e496ba-90e7-431e-8877-a0aef6c6da28">

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