Bug: Mark retry payment as failure if `connector_tokenization` fails



When a payment confirm call is made the status of the payment would be `requires_payment_method` while performing connector pre-processing steps. If a there is an error during this step then the status remains in `requires_payment_method`. If the pre-processing steps succeeds, the status is changed to `processing` just before the authorize flow. Now if the status will be further updated based on the connector response.

Now if there is an failure response from the connector and if the the retry feature is enabled for that merchant we go to the retry flow. Now for the chosen connector we perform the pre-processing steps, here if something fails the payment status remains in `processing` as previous during the first attempt the status was updated to  `processing` during the authorize flow.

In this case if something fails in the pre-processing steps during the retry we need to fail the payment.
diff --git a/crates/router/src/connector/stripe/transformers.rs b/crates/router/src/connector/stripe/transformers.rs
index 10d091a3b84..53850e97afc 100644
--- a/crates/router/src/connector/stripe/transformers.rs
+++ b/crates/router/src/connector/stripe/transformers.rs
@@ -33,7 +33,6 @@ use crate::{
         storage::enums,
         transformers::{ForeignFrom, ForeignTryFrom},
     },
-    unimplemented_payment_method,
     utils::OptionExt,
 };
 
@@ -1761,9 +1760,11 @@ impl TryFrom<(&types::PaymentsAuthorizeRouterData, MinorUnit)> for PaymentIntent
 
                 let payment_method_token = match payment_method_token {
                     types::PaymentMethodToken::Token(payment_method_token) => payment_method_token,
-                    types::PaymentMethodToken::ApplePayDecrypt(_) => Err(
-                        unimplemented_payment_method!("Apple Pay", "Simplified", "Stripe"),
-                    )?,
+                    types::PaymentMethodToken::ApplePayDecrypt(_) => {
+                        Err(errors::ConnectorError::InvalidWalletToken {
+                            wallet_name: "Apple Pay".to_string(),
+                        })?
+                    }
                 };
                 Some(StripePaymentMethodData::Wallet(
                     StripeWallet::ApplepayPayment(ApplepayPayment {
diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs
index 8290177f21b..69c55b152ed 100644
--- a/crates/router/src/core/payments.rs
+++ b/crates/router/src/core/payments.rs
@@ -302,6 +302,7 @@ where
                         #[cfg(not(feature = "frm"))]
                         None,
                         &business_profile,
+                        false,
                     )
                     .await?;
 
@@ -373,6 +374,7 @@ where
                         #[cfg(not(feature = "frm"))]
                         None,
                         &business_profile,
+                        false,
                     )
                     .await?;
 
@@ -1394,6 +1396,7 @@ pub async fn call_connector_service<F, RouterDReq, ApiRequest>(
     header_payload: HeaderPayload,
     frm_suggestion: Option<storage_enums::FrmSuggestion>,
     business_profile: &storage::business_profile::BusinessProfile,
+    is_retry_payment: bool,
 ) -> RouterResult<RouterData<F, RouterDReq, router_types::PaymentsResponseData>>
 where
     F: Send + Clone + Sync,
@@ -1476,7 +1479,7 @@ where
 
     router_data = router_data.add_session_token(state, &connector).await?;
 
-    let mut should_continue_further = access_token::update_router_data_with_access_token_result(
+    let should_continue_further = access_token::update_router_data_with_access_token_result(
         &add_access_token_result,
         &mut router_data,
         &call_connector_action,
@@ -1526,16 +1529,22 @@ where
         _ => (),
     };
 
-    let pm_token = router_data
-        .add_payment_method_token(state, &connector, &tokenization_action)
+    let payment_method_token_response = router_data
+        .add_payment_method_token(
+            state,
+            &connector,
+            &tokenization_action,
+            should_continue_further,
+        )
         .await?;
-    if let Some(payment_method_token) = pm_token.clone() {
-        router_data.payment_method_token = Some(
-            hyperswitch_domain_models::router_data::PaymentMethodToken::Token(Secret::new(
-                payment_method_token,
-            )),
+
+    let mut should_continue_further =
+        tokenization::update_router_data_with_payment_method_token_result(
+            payment_method_token_response,
+            &mut router_data,
+            is_retry_payment,
+            should_continue_further,
         );
-    };
 
     (router_data, should_continue_further) = complete_preprocessing_steps_if_required(
         state,
diff --git a/crates/router/src/core/payments/flows.rs b/crates/router/src/core/payments/flows.rs
index 9ecd0c4c8a0..23ab253ee53 100644
--- a/crates/router/src/core/payments/flows.rs
+++ b/crates/router/src/core/payments/flows.rs
@@ -84,13 +84,17 @@ pub trait Feature<F, T> {
         _state: &SessionState,
         _connector: &api::ConnectorData,
         _tokenization_action: &payments::TokenizationAction,
-    ) -> RouterResult<Option<String>>
+        _should_continue_payment: bool,
+    ) -> RouterResult<types::PaymentMethodTokenResult>
     where
         F: Clone,
         Self: Sized,
         dyn api::Connector: services::ConnectorIntegration<F, T, types::PaymentsResponseData>,
     {
-        Ok(None)
+        Ok(types::PaymentMethodTokenResult {
+            payment_method_token_result: Ok(None),
+            is_payment_method_tokenization_performed: false,
+        })
     }
 
     async fn preprocessing_steps<'a>(
diff --git a/crates/router/src/core/payments/flows/authorize_flow.rs b/crates/router/src/core/payments/flows/authorize_flow.rs
index 23971f3696c..c06d9a70e16 100644
--- a/crates/router/src/core/payments/flows/authorize_flow.rs
+++ b/crates/router/src/core/payments/flows/authorize_flow.rs
@@ -140,7 +140,8 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
         state: &SessionState,
         connector: &api::ConnectorData,
         tokenization_action: &payments::TokenizationAction,
-    ) -> RouterResult<Option<String>> {
+        should_continue_payment: bool,
+    ) -> RouterResult<types::PaymentMethodTokenResult> {
         let request = self.request.clone();
         tokenization::add_payment_method_token(
             state,
@@ -148,6 +149,7 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
             tokenization_action,
             self,
             types::PaymentMethodTokenizationData::try_from(request)?,
+            should_continue_payment,
         )
         .await
     }
diff --git a/crates/router/src/core/payments/flows/complete_authorize_flow.rs b/crates/router/src/core/payments/flows/complete_authorize_flow.rs
index bb56cb5a7c5..6bef7e95859 100644
--- a/crates/router/src/core/payments/flows/complete_authorize_flow.rs
+++ b/crates/router/src/core/payments/flows/complete_authorize_flow.rs
@@ -103,7 +103,8 @@ impl Feature<api::CompleteAuthorize, types::CompleteAuthorizeData>
         state: &SessionState,
         connector: &api::ConnectorData,
         _tokenization_action: &payments::TokenizationAction,
-    ) -> RouterResult<Option<String>> {
+        should_continue_payment: bool,
+    ) -> RouterResult<types::PaymentMethodTokenResult> {
         // TODO: remove this and handle it in core
         if matches!(connector.connector_name, types::Connector::Payme) {
             let request = self.request.clone();
@@ -113,10 +114,14 @@ impl Feature<api::CompleteAuthorize, types::CompleteAuthorizeData>
                 &payments::TokenizationAction::TokenizeInConnector,
                 self,
                 types::PaymentMethodTokenizationData::try_from(request)?,
+                should_continue_payment,
             )
             .await
         } else {
-            Ok(None)
+            Ok(types::PaymentMethodTokenResult {
+                payment_method_token_result: Ok(None),
+                is_payment_method_tokenization_performed: false,
+            })
         }
     }
 
diff --git a/crates/router/src/core/payments/flows/setup_mandate_flow.rs b/crates/router/src/core/payments/flows/setup_mandate_flow.rs
index 07803a0c0fb..773677f3e7c 100644
--- a/crates/router/src/core/payments/flows/setup_mandate_flow.rs
+++ b/crates/router/src/core/payments/flows/setup_mandate_flow.rs
@@ -107,7 +107,8 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
         state: &SessionState,
         connector: &api::ConnectorData,
         tokenization_action: &payments::TokenizationAction,
-    ) -> RouterResult<Option<String>> {
+        should_continue_payment: bool,
+    ) -> RouterResult<types::PaymentMethodTokenResult> {
         let request = self.request.clone();
         tokenization::add_payment_method_token(
             state,
@@ -115,6 +116,7 @@ impl Feature<api::SetupMandate, types::SetupMandateRequestData> for types::Setup
             tokenization_action,
             self,
             types::PaymentMethodTokenizationData::try_from(request)?,
+            should_continue_payment,
         )
         .await
     }
diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs
index e8ceeaa7ecf..ef8e0c01fed 100644
--- a/crates/router/src/core/payments/retry.rs
+++ b/crates/router/src/core/payments/retry.rs
@@ -321,6 +321,7 @@ where
         api::HeaderPayload::default(),
         frm_suggestion,
         business_profile,
+        true,
     )
     .await
 }
diff --git a/crates/router/src/core/payments/tokenization.rs b/crates/router/src/core/payments/tokenization.rs
index d6ceeee65e7..da18fd7dd5a 100644
--- a/crates/router/src/core/payments/tokenization.rs
+++ b/crates/router/src/core/payments/tokenization.rs
@@ -784,62 +784,109 @@ pub async fn add_payment_method_token<F: Clone, T: types::Tokenizable + Clone>(
     tokenization_action: &payments::TokenizationAction,
     router_data: &mut types::RouterData<F, T, types::PaymentsResponseData>,
     pm_token_request_data: types::PaymentMethodTokenizationData,
-) -> RouterResult<Option<String>> {
-    match tokenization_action {
-        payments::TokenizationAction::TokenizeInConnector
-        | payments::TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(_) => {
-            let connector_integration: services::BoxedPaymentConnectorIntegrationInterface<
-                api::PaymentMethodToken,
-                types::PaymentMethodTokenizationData,
-                types::PaymentsResponseData,
-            > = connector.connector.get_connector_integration();
-
-            let pm_token_response_data: Result<types::PaymentsResponseData, types::ErrorResponse> =
-                Err(types::ErrorResponse::default());
-
-            let pm_token_router_data =
-                helpers::router_data_type_conversion::<_, api::PaymentMethodToken, _, _, _, _>(
-                    router_data.clone(),
-                    pm_token_request_data,
-                    pm_token_response_data,
+    should_continue_payment: bool,
+) -> RouterResult<types::PaymentMethodTokenResult> {
+    if should_continue_payment {
+        match tokenization_action {
+            payments::TokenizationAction::TokenizeInConnector
+            | payments::TokenizationAction::TokenizeInConnectorAndApplepayPreDecrypt(_) => {
+                let connector_integration: services::BoxedPaymentConnectorIntegrationInterface<
+                    api::PaymentMethodToken,
+                    types::PaymentMethodTokenizationData,
+                    types::PaymentsResponseData,
+                > = connector.connector.get_connector_integration();
+
+                let pm_token_response_data: Result<
+                    types::PaymentsResponseData,
+                    types::ErrorResponse,
+                > = Err(types::ErrorResponse::default());
+
+                let pm_token_router_data =
+                    helpers::router_data_type_conversion::<_, api::PaymentMethodToken, _, _, _, _>(
+                        router_data.clone(),
+                        pm_token_request_data,
+                        pm_token_response_data,
+                    );
+
+                router_data
+                    .request
+                    .set_session_token(pm_token_router_data.session_token.clone());
+
+                let resp = services::execute_connector_processing_step(
+                    state,
+                    connector_integration,
+                    &pm_token_router_data,
+                    payments::CallConnectorAction::Trigger,
+                    None,
+                )
+                .await
+                .to_payment_failed_response()?;
+
+                metrics::CONNECTOR_PAYMENT_METHOD_TOKENIZATION.add(
+                    &metrics::CONTEXT,
+                    1,
+                    &add_attributes([
+                        ("connector", connector.connector_name.to_string()),
+                        ("payment_method", router_data.payment_method.to_string()),
+                    ]),
                 );
 
-            router_data
-                .request
-                .set_session_token(pm_token_router_data.session_token.clone());
-
-            let resp = services::execute_connector_processing_step(
-                state,
-                connector_integration,
-                &pm_token_router_data,
-                payments::CallConnectorAction::Trigger,
-                None,
-            )
-            .await
-            .to_payment_failed_response()?;
-
-            metrics::CONNECTOR_PAYMENT_METHOD_TOKENIZATION.add(
-                &metrics::CONTEXT,
-                1,
-                &add_attributes([
-                    ("connector", connector.connector_name.to_string()),
-                    ("payment_method", router_data.payment_method.to_string()),
-                ]),
-            );
-
-            let pm_token = match resp.response {
-                Ok(response) => match response {
-                    types::PaymentsResponseData::TokenizationResponse { token } => Some(token),
-                    _ => None,
-                },
-                Err(err) => {
+                let payment_token_resp = resp.response.map(|res| {
+                    if let types::PaymentsResponseData::TokenizationResponse { token } = res {
+                        Some(token)
+                    } else {
+                        None
+                    }
+                });
+
+                Ok(types::PaymentMethodTokenResult {
+                    payment_method_token_result: payment_token_resp,
+                    is_payment_method_tokenization_performed: true,
+                })
+            }
+            _ => Ok(types::PaymentMethodTokenResult {
+                payment_method_token_result: Ok(None),
+                is_payment_method_tokenization_performed: false,
+            }),
+        }
+    } else {
+        logger::debug!("Skipping connector tokenization based on should_continue_payment flag");
+        Ok(types::PaymentMethodTokenResult {
+            payment_method_token_result: Ok(None),
+            is_payment_method_tokenization_performed: false,
+        })
+    }
+}
+
+pub fn update_router_data_with_payment_method_token_result<F: Clone, T>(
+    payment_method_token_result: types::PaymentMethodTokenResult,
+    router_data: &mut types::RouterData<F, T, types::PaymentsResponseData>,
+    is_retry_payment: bool,
+    should_continue_further: bool,
+) -> bool {
+    if payment_method_token_result.is_payment_method_tokenization_performed {
+        match payment_method_token_result.payment_method_token_result {
+            Ok(pm_token_result) => {
+                router_data.payment_method_token = pm_token_result.map(|pm_token| {
+                    hyperswitch_domain_models::router_data::PaymentMethodToken::Token(
+                        masking::Secret::new(pm_token),
+                    )
+                });
+
+                true
+            }
+            Err(err) => {
+                if is_retry_payment {
+                    router_data.response = Err(err);
+                    false
+                } else {
                     logger::debug!(payment_method_tokenization_error=?err);
-                    None
+                    true
                 }
-            };
-            Ok(pm_token)
+            }
         }
-        _ => Ok(None),
+    } else {
+        should_continue_further
     }
 }
 
diff --git a/crates/router/src/types.rs b/crates/router/src/types.rs
index 607e5bd8234..f0087ef2f39 100644
--- a/crates/router/src/types.rs
+++ b/crates/router/src/types.rs
@@ -545,6 +545,11 @@ pub struct AddAccessTokenResult {
     pub connector_supports_access_token: bool,
 }
 
+pub struct PaymentMethodTokenResult {
+    pub payment_method_token_result: Result<Option<String>, ErrorResponse>,
+    pub is_payment_method_tokenization_performed: bool,
+}
+
 #[derive(Debug, Clone, Copy)]
 pub enum Redirection {
     Redirect,

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

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

## Description
<!-- Describe your changes in detail -->
When a payment confirm call is made the status of the payment would be `requires_payment_method` while performing connector pre-processing steps. If a there is an error during this step then the status remains in `requires_payment_method`. If the pre-processing steps succeeds, the status is changed to `processing` just before the authorize flow. Now if the status will be further updated based on the connector response.

Now if there is an failure response from the connector and if the the retry feature is enabled for that merchant we go to the retry flow. Now for the chosen connector we perform the pre-processing steps, here if something fails the payment status remains in `processing` as previous during the first attempt the status was updated to  `processing` during the authorize flow.

In this case if something fails in the pre-processing steps during the retry we need to fail the payment.

### 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)?
-->
-> Create MCA of stripe with apple pay simplified flow enabled
-> The connector api in the connector tokenizatoin flow is hardcoded wrongly for test purpose. So in this case if apple pay simplified payment request is made, the error response changes to `Invalid wallet token` from `unimplemented flow`. And also in the db the `payment_intent` status will be as `requires_payment_method`.
<img width="1058" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/1fd17ca1-5d82-455a-b92a-3c96ea4ab41d">
<img width="1263" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/8289bd07-2d5a-4887-8874-49ce13001ba5">

-> Configure one more connector (`BOA`) with simplified flow. And configure the routing rule such that the payment first go through `BOA` and retry happens through `Stripe`.

-> Enable retry for the merchant account.
```
curl --location 'localhost:8080/configs/' \
--header 'api-key: test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "key": "should_call_gsm_merchant_1719384197",
    "value": "true"
}'
```

```
curl --location 'localhost:8080/configs/' \
--header 'api-key: test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "key": "max_auto_retries_enabled_merchant_1719384197",
    "value": "2"
}'
```

```
curl --location 'localhost:8080/gsm' \
--header 'api-key: test_admin' \
--header 'Content-Type: application/json' \
--data '{
    "connector": "bankofamerica",
    "flow": "Authorize",
    "sub_flow": "sub_flow",
    "code": "No error code",
    "message": "Authentication Failed",
    "status": "Failure",
    "decision": "retry",
    "step_up_possible": false
}'
```

-> Now I have hardcoded the `BOA` api key wrongly so that payment fails and will be retried with `Stripe`. Also the connector api in the connector tokenizatoin flow is hardcoded wrongly in `Stripe` to test if the payment reaches a terminal state (`failed`) if that connector tokenization call fails for stripe.
```
{
    "amount": 650,
    "currency": "USD",
    "confirm": true,
    "business_country": "US",
    "business_label": "default",
    "amount_to_capture": 650,
    "customer_id": "test_fb",
    "capture_method": "automatic",
    "capture_on": "2022-09-10T10:11:12Z",
    "authentication_type": "three_ds",
    "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",
    "payment_method": "wallet",
    "payment_method_type": "apple_pay",
    "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"
        }
    },
    "payment_method_data": {
        "wallet": {
            "apple_pay": {
                "payment_data": "eyJkYXRhIjoiVnJnTm9mYXRRZEFIME1MTjZjd1VMazdIS09PdWIvUnMwbEtFOW1vWllvcFhSSXpGemM4VDJiMzlpWjdWSGVXK0YxZVFmY0V5OFJrWjMvcWRZQkVyaE4wK1BxL2pRZ2xpZFNUNlYzdXNwRHB6MlJGSWxYUWhiODJndmd5Y3hSb1R6MER4MDgwNzQ1MUJkN1RMUldyVWNlaVRvdEN4N041ZXFVcjM3UTJlZ2hTaExNOWtXVzAxSmIvT3FLM3RLTUhZaFZNRmE2alNaaHduMVU1NitpbXR5SDMvS1BpdlJlQ0ZxTUtFVmJiRFRyNmpzSDEyNWUwVmlOalBXYm1kVzZJZWU2RDR0cjk5VEhiSkhiWUJzaVdrMUROb29tS28zbHRVUjRkMTZwL09Cc2xTYUh6dnFTL2VrOGVCTDB4dTB3TkMyamx5OFpQUnVZNGRZL2gyS3U0VmprL1BpTWtJUnRNVGl3MTFZWExXUHFNamkvM1N1VDcxUEFnZllpQU4zZmxYK1Q5RmVZRE5yVmdSNWFyRGN3PT0iLCJzaWduYXR1cmUiOiJNSUFHQ1NxR1NJYjNEUUVIQXFDQU1JQUNBUUV4RFRBTEJnbGdoa2dCWlFNRUFnRXdnQVlKS29aSWh2Y05BUWNCQUFDZ2dEQ0NBK013Z2dPSW9BTUNBUUlDQ0JaalRJc09NRmNYTUFvR0NDcUdTTTQ5QkFNQ01Ib3hMakFzQmdOVkJwYjI0Z1EwRWdMU0JITXpFbU1DUUdBMVVFQ3d3ZFFYQndiR1VnUTJWeWRHbG1hV05oZEdsdmJpQkJkWFJvYjNKcGRIa3hFekFSQmdOVkJBb01Da0Z3Y0d4bElFbHVZeTR4Q3pBSkJnTlZCQVlUQWxWVE1Ga3dFd1lIS29aSXpqMENBUVlJS29aSXpqMERBUWNEUWdBRThCY1JoQm5YWklYVkdsNGxnUWQyNklDaTc5NTdyazNnamZ4TGsrRXpWdFZtV3pXdUl0Q1hkZzBpVG51NkNQMTJGODZJeTNhN1puQyt5T2dwaFA5VVJhT0I5ekNCOURCR0JnZ3JCZ0VGQlFjQkFRUTZNRGd3TmdZSUt3WUJCUVVITUFRpb25JZCI6ImMxMTFjYzA1YzBkZjFiMjZmYTI3MjcwZDcyMjhmM2EzNDQ1ZTJhMDNiYTMyYjQ5NjI3ODEzNTVkYjc2M2VjNjMifSwidmVyc2lvbiI6IkVDX3YxIn0=",
                "payment_method": {
                    "display_name": "Visa 0326",
                    "network": "Mastercard",
                    "type": "debit"
                },
                "transaction_identifier": "55CC32D7BF7890B9064433F15B9F23F849CF84AFD01E4E65DD8ADE306300E9D8"
            }
        }
    }
}
```
<img width="1120" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/d67a32d6-915b-46ca-b257-4a622897f005">
<img width="1198" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/3ddc4fce-fa8c-4e7c-b91e-840d938f9dbf">
<img width="1397" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/7608db87-7610-4355-8dbb-584d1953984e">

-> When made the same above request without the changes in this pr.
<img width="830" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/574fff8f-875b-4416-8272-a026f42853a9">

<img width="1117" alt="image" src="https://github.com/juspay/hyperswitch/assets/83439957/ce8a6a3f-61d7-4d53-823b-c3f863fcad2e">


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