Bug: [Bug] Update Redis TTL for customer locks after token selection



The revenue recovery workflow currently does not update the Redis TTL for a connector customer lock after the decider service selects the best PSP token. This behavior can lead to a customer lock expiring prematurely, which breaks the consistency of the recovery process.
diff --git a/config/config.example.toml b/config/config.example.toml
index b725be102c1..4ce52dfac4f 100644
--- a/config/config.example.toml
+++ b/config/config.example.toml
@@ -1241,6 +1241,7 @@ initial_timestamp_in_seconds = 3600          # number of seconds added to start
 job_schedule_buffer_time_in_seconds = 3600    # buffer time in seconds to schedule the job for Revenue Recovery
 reopen_workflow_buffer_time_in_seconds = 3600  # time in seconds to be added in scheduling for calculate workflow
 max_random_schedule_delay_in_seconds = 300  # max random delay in seconds to schedule the payment for Revenue Recovery
+redis_ttl_buffer_in_seconds= 300 # buffer time in seconds to be added to redis ttl for Revenue Recovery
 
 [clone_connector_allowlist]
 merchant_ids = "merchant_ids"           # Comma-separated list of allowed merchant IDs
diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml
index bfba035c1f3..726bdbe21a9 100644
--- a/config/deployments/env_specific.toml
+++ b/config/deployments/env_specific.toml
@@ -415,6 +415,7 @@ initial_timestamp_in_seconds = 3600        # number of seconds added to start ti
 job_schedule_buffer_time_in_seconds = 3600 # time in seconds to be added in schedule time as a buffer 
 reopen_workflow_buffer_time_in_seconds = 3600  # time in seconds to be added in scheduling for calculate workflow
 max_random_schedule_delay_in_seconds = 300 # max random delay in seconds to schedule the payment for Revenue Recovery
+redis_ttl_buffer_in_seconds=300 # buffer time in seconds to be added to redis ttl for Revenue Recovery
 
 [chat]
 enabled = false                                # Enable or disable chat features
diff --git a/config/development.toml b/config/development.toml
index 940e43b50ca..07cadbf3058 100644
--- a/config/development.toml
+++ b/config/development.toml
@@ -1340,6 +1340,7 @@ initial_timestamp_in_seconds = 3600
 job_schedule_buffer_time_in_seconds = 3600
 reopen_workflow_buffer_time_in_seconds = 3600
 max_random_schedule_delay_in_seconds = 300
+redis_ttl_buffer_in_seconds= 300
 
 [revenue_recovery.card_config.amex]
 max_retries_per_day = 20
diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs
index 3844fa34bc4..b0b656a1ede 100644
--- a/crates/api_models/src/admin.rs
+++ b/crates/api_models/src/admin.rs
@@ -2718,6 +2718,11 @@ pub struct ProfileResponse {
     /// Enable split payments, i.e., split the amount between multiple payment methods
     #[schema(value_type = SplitTxnsEnabled, default = "skip")]
     pub split_txns_enabled: common_enums::SplitTxnsEnabled,
+
+    /// Indicates the state of revenue recovery algorithm type
+    #[schema(value_type = Option<RevenueRecoveryAlgorithmType>, example = "cascading")]
+    pub revenue_recovery_retry_algorithm_type:
+        Option<common_enums::enums::RevenueRecoveryAlgorithmType>,
 }
 
 #[cfg(feature = "v1")]
diff --git a/crates/openapi/src/openapi_v2.rs b/crates/openapi/src/openapi_v2.rs
index c0b042b182f..71fe216322d 100644
--- a/crates/openapi/src/openapi_v2.rs
+++ b/crates/openapi/src/openapi_v2.rs
@@ -801,6 +801,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::payment_methods::PaymentMethodSessionResponse,
         api_models::payment_methods::AuthenticationDetails,
         api_models::process_tracker::revenue_recovery::RevenueRecoveryResponse,
+        api_models::enums::RevenueRecoveryAlgorithmType,
         api_models::enums::ProcessTrackerStatus,
         api_models::proxy::ProxyRequest,
         api_models::proxy::ProxyResponse,
diff --git a/crates/router/src/core/revenue_recovery.rs b/crates/router/src/core/revenue_recovery.rs
index d63cbd5f906..f714564e1f3 100644
--- a/crates/router/src/core/revenue_recovery.rs
+++ b/crates/router/src/core/revenue_recovery.rs
@@ -671,8 +671,9 @@ async fn update_calculate_job_schedule_time(
     base_time: Option<time::PrimitiveDateTime>,
     connector_customer_id: &str,
 ) -> Result<(), sch_errors::ProcessTrackerError> {
-    let new_schedule_time =
-        base_time.unwrap_or_else(common_utils::date_time::now) + additional_time;
+    let now = common_utils::date_time::now();
+
+    let new_schedule_time = base_time.filter(|&t| t > now).unwrap_or(now) + additional_time;
     logger::info!(
         new_schedule_time = %new_schedule_time,
         process_id = %process.id,
diff --git a/crates/router/src/core/revenue_recovery/types.rs b/crates/router/src/core/revenue_recovery/types.rs
index f63259b0c85..40532480d38 100644
--- a/crates/router/src/core/revenue_recovery/types.rs
+++ b/crates/router/src/core/revenue_recovery/types.rs
@@ -806,17 +806,13 @@ impl Action {
                         .change_context(errors::RecoveryError::ValueNotFound)
                         .attach_printable("Failed to extract customer ID from payment intent")?;
 
-                    let is_hard_decline =
-                        revenue_recovery::check_hard_decline(state, &payment_attempt)
-                            .await
-                            .ok();
-
                     // update the status of token in redis
                     let _update_error_code = storage::revenue_recovery_redis_operation::RedisTokenManager::update_payment_processor_token_error_code_from_process_tracker(
                     state,
                     &connector_customer_id,
                     &None,
-                    &is_hard_decline,
+                    // Since this is succeeded, 'hard_decine' will be false.
+                    &Some(false),
                     used_token.as_deref(),
                 )
                 .await;
@@ -900,13 +896,25 @@ impl Action {
         logger::info!("Entering psync_response_handler");
 
         let db = &*state.store;
+
+        let connector_customer_id = payment_intent
+            .feature_metadata
+            .as_ref()
+            .and_then(|fm| fm.payment_revenue_recovery_metadata.as_ref())
+            .map(|rr| {
+                rr.billing_connector_payment_details
+                    .connector_customer_id
+                    .clone()
+            });
+
         match self {
             Self::SyncPayment(payment_attempt) => {
                 //  get a schedule time for psync
                 // and retry the process if there is a schedule time
                 // if None mark the pt status as Retries Exceeded and finish the task
-                payment_sync::retry_sync_task(
-                    db,
+                payment_sync::recovery_retry_sync_task(
+                    state,
+                    connector_customer_id,
                     revenue_recovery_metadata.connector.to_string(),
                     revenue_recovery_payment_data
                         .merchant_account
diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs
index 2576bfbd8fc..0e8cb60ff81 100644
--- a/crates/router/src/types/api/admin.rs
+++ b/crates/router/src/types/api/admin.rs
@@ -322,6 +322,7 @@ impl ForeignTryFrom<domain::Profile> for ProfileResponse {
             merchant_category_code: item.merchant_category_code,
             merchant_country_code: item.merchant_country_code,
             split_txns_enabled: item.split_txns_enabled,
+            revenue_recovery_retry_algorithm_type: item.revenue_recovery_retry_algorithm_type,
         })
     }
 }
diff --git a/crates/router/src/types/storage/revenue_recovery.rs b/crates/router/src/types/storage/revenue_recovery.rs
index 5a3f9f1bf7e..48110eb92ed 100644
--- a/crates/router/src/types/storage/revenue_recovery.rs
+++ b/crates/router/src/types/storage/revenue_recovery.rs
@@ -81,6 +81,7 @@ pub struct RecoveryTimestamp {
     pub job_schedule_buffer_time_in_seconds: i64,
     pub reopen_workflow_buffer_time_in_seconds: i64,
     pub max_random_schedule_delay_in_seconds: i64,
+    pub redis_ttl_buffer_in_seconds: i64,
 }
 
 impl Default for RecoveryTimestamp {
@@ -90,6 +91,7 @@ impl Default for RecoveryTimestamp {
             job_schedule_buffer_time_in_seconds: 15,
             reopen_workflow_buffer_time_in_seconds: 60,
             max_random_schedule_delay_in_seconds: 300,
+            redis_ttl_buffer_in_seconds: 300,
         }
     }
 }
diff --git a/crates/router/src/types/storage/revenue_recovery_redis_operation.rs b/crates/router/src/types/storage/revenue_recovery_redis_operation.rs
index e4eec924a98..f083e8bbbed 100644
--- a/crates/router/src/types/storage/revenue_recovery_redis_operation.rs
+++ b/crates/router/src/types/storage/revenue_recovery_redis_operation.rs
@@ -61,12 +61,22 @@ pub struct PaymentProcessorTokenWithRetryInfo {
     pub retry_wait_time_hours: i64,
     /// Number of retries remaining in the 30-day rolling window
     pub monthly_retry_remaining: i32,
+    // Current total retry count in 30-day window
+    pub total_30_day_retries: i32,
 }
 
 /// Redis-based token management struct
 pub struct RedisTokenManager;
 
 impl RedisTokenManager {
+    fn get_connector_customer_lock_key(connector_customer_id: &str) -> String {
+        format!("customer:{connector_customer_id}:status")
+    }
+
+    fn get_connector_customer_tokens_key(connector_customer_id: &str) -> String {
+        format!("customer:{connector_customer_id}:tokens")
+    }
+
     /// Lock connector customer
     #[instrument(skip_all)]
     pub async fn lock_connector_customer_status(
@@ -82,7 +92,7 @@ impl RedisTokenManager {
                     errors::RedisError::RedisConnectionError.into(),
                 ))?;
 
-        let lock_key = format!("customer:{connector_customer_id}:status");
+        let lock_key = Self::get_connector_customer_lock_key(connector_customer_id);
         let seconds = &state.conf.revenue_recovery.redis_ttl_in_seconds;
 
         let result: bool = match redis_conn
@@ -109,6 +119,42 @@ impl RedisTokenManager {
 
         Ok(result)
     }
+    #[instrument(skip_all)]
+    pub async fn update_connector_customer_lock_ttl(
+        state: &SessionState,
+        connector_customer_id: &str,
+        exp_in_seconds: i64,
+    ) -> CustomResult<bool, errors::StorageError> {
+        let redis_conn =
+            state
+                .store
+                .get_redis_conn()
+                .change_context(errors::StorageError::RedisError(
+                    errors::RedisError::RedisConnectionError.into(),
+                ))?;
+
+        let lock_key = Self::get_connector_customer_lock_key(connector_customer_id);
+
+        let result: bool = redis_conn
+            .set_expiry(&lock_key.into(), exp_in_seconds)
+            .await
+            .map_or_else(
+                |error| {
+                    tracing::error!(operation = "update_lock_ttl", err = ?error);
+                    false
+                },
+                |_| true,
+            );
+
+        tracing::debug!(
+            connector_customer_id = connector_customer_id,
+            new_ttl_in_seconds = exp_in_seconds,
+            ttl_updated = %result,
+            "Connector customer lock TTL update with new expiry time"
+        );
+
+        Ok(result)
+    }
 
     /// Unlock connector customer status
     #[instrument(skip_all)]
@@ -124,7 +170,7 @@ impl RedisTokenManager {
                     errors::RedisError::RedisConnectionError.into(),
                 ))?;
 
-        let lock_key = format!("customer:{connector_customer_id}:status");
+        let lock_key = Self::get_connector_customer_lock_key(connector_customer_id);
 
         match redis_conn.delete_key(&lock_key.into()).await {
             Ok(DelReply::KeyDeleted) => {
@@ -158,7 +204,7 @@ impl RedisTokenManager {
                 .change_context(errors::StorageError::RedisError(
                     errors::RedisError::RedisConnectionError.into(),
                 ))?;
-        let tokens_key = format!("customer:{connector_customer_id}:tokens");
+        let tokens_key = Self::get_connector_customer_tokens_key(connector_customer_id);
 
         let get_hash_err =
             errors::StorageError::RedisError(errors::RedisError::GetHashFieldFailed.into());
@@ -209,7 +255,7 @@ impl RedisTokenManager {
                 .change_context(errors::StorageError::RedisError(
                     errors::RedisError::RedisConnectionError.into(),
                 ))?;
-        let tokens_key = format!("customer:{connector_customer_id}:tokens");
+        let tokens_key = Self::get_connector_customer_tokens_key(connector_customer_id);
 
         // allocate capacity up-front to avoid rehashing
         let mut serialized_payment_processor_tokens: HashMap<String, String> =
@@ -312,15 +358,18 @@ impl RedisTokenManager {
             // Obtain network-specific limits and compute remaining monthly retries.
             let card_network_config = card_config.get_network_config(card_network);
 
-            let monthly_retry_remaining = card_network_config
-                .max_retry_count_for_thirty_day
-                .saturating_sub(retry_info.total_30_day_retries);
+            let monthly_retry_remaining = std::cmp::max(
+                0,
+                card_network_config.max_retry_count_for_thirty_day
+                    - retry_info.total_30_day_retries,
+            );
 
             // Build the per-token result struct.
             let token_with_retry_info = PaymentProcessorTokenWithRetryInfo {
                 token_status: payment_processor_token_status.clone(),
                 retry_wait_time_hours,
                 monthly_retry_remaining,
+                total_30_day_retries: retry_info.total_30_day_retries,
             };
 
             result.insert(payment_processor_token_id.clone(), token_with_retry_info);
@@ -367,6 +416,7 @@ impl RedisTokenManager {
         let monthly_wait_hours =
             if total_30_day_retries >= card_network_config.max_retry_count_for_thirty_day {
                 (0..RETRY_WINDOW_DAYS)
+                    .rev()
                     .map(|i| today - Duration::days(i.into()))
                     .find(|date| token.daily_retry_history.get(date).copied().unwrap_or(0) > 0)
                     .map(|date| Self::calculate_wait_hours(date + Duration::days(31), now))
diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs
index d7ac92e131e..f0bd7e3619d 100644
--- a/crates/router/src/workflows/payment_sync.rs
+++ b/crates/router/src/workflows/payment_sync.rs
@@ -1,3 +1,5 @@
+#[cfg(feature = "v2")]
+use common_utils::ext_traits::AsyncExt;
 use common_utils::ext_traits::{OptionExt, StringExt, ValueExt};
 use diesel_models::process_tracker::business_status;
 use error_stack::ResultExt;
@@ -7,6 +9,8 @@ use scheduler::{
     errors as sch_errors, utils as scheduler_utils,
 };
 
+#[cfg(feature = "v2")]
+use crate::workflows::revenue_recovery::update_token_expiry_based_on_schedule_time;
 use crate::{
     consts,
     core::{
@@ -304,6 +308,51 @@ pub async fn retry_sync_task(
     }
 }
 
+/// Schedule the task for retry and update redis token expiry time
+///
+/// Returns bool which indicates whether this was the last retry or not
+#[cfg(feature = "v2")]
+pub async fn recovery_retry_sync_task(
+    state: &SessionState,
+    connector_customer_id: Option<String>,
+    connector: String,
+    merchant_id: common_utils::id_type::MerchantId,
+    pt: storage::ProcessTracker,
+) -> Result<bool, sch_errors::ProcessTrackerError> {
+    let db = &*state.store;
+    let schedule_time =
+        get_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count + 1).await?;
+
+    match schedule_time {
+        Some(s_time) => {
+            db.as_scheduler().retry_process(pt, s_time).await?;
+
+            connector_customer_id
+                .async_map(|id| async move {
+                    let _ = update_token_expiry_based_on_schedule_time(state, &id, Some(s_time))
+                        .await
+                        .map_err(|e| {
+                            logger::error!(
+                                error = ?e,
+                                connector_customer_id = %id,
+                                "Failed to update the token expiry time in redis"
+                            );
+                            e
+                        });
+                })
+                .await;
+
+            Ok(false)
+        }
+        None => {
+            db.as_scheduler()
+                .finish_process_with_business_status(pt, business_status::RETRIES_EXCEEDED)
+                .await?;
+            Ok(true)
+        }
+    }
+}
+
 #[cfg(test)]
 mod tests {
     #![allow(clippy::expect_used, clippy::unwrap_used)]
diff --git a/crates/router/src/workflows/revenue_recovery.rs b/crates/router/src/workflows/revenue_recovery.rs
index b85ebd3ff11..6a6d9511062 100644
--- a/crates/router/src/workflows/revenue_recovery.rs
+++ b/crates/router/src/workflows/revenue_recovery.rs
@@ -371,10 +371,7 @@ pub(crate) async fn get_schedule_time_for_smart_retry(
         card_network: card_network_str,
         card_issuer: card_issuer_str,
         invoice_start_time: Some(start_time_proto),
-        retry_count: Some(
-            (total_retry_count_within_network.max_retry_count_for_thirty_day - retry_count_left)
-                .into(),
-        ),
+        retry_count: Some(token_with_retry_info.total_30_day_retries.into()),
         merchant_id,
         invoice_amount,
         invoice_currency,
@@ -513,6 +510,47 @@ pub struct ScheduledToken {
     pub schedule_time: time::PrimitiveDateTime,
 }
 
+#[cfg(feature = "v2")]
+pub fn calculate_difference_in_seconds(scheduled_time: time::PrimitiveDateTime) -> i64 {
+    let now_utc = time::OffsetDateTime::now_utc();
+
+    let scheduled_offset_dt = scheduled_time.assume_utc();
+    let difference = scheduled_offset_dt - now_utc;
+
+    difference.whole_seconds()
+}
+
+#[cfg(feature = "v2")]
+pub async fn update_token_expiry_based_on_schedule_time(
+    state: &SessionState,
+    connector_customer_id: &str,
+    delayed_schedule_time: Option<time::PrimitiveDateTime>,
+) -> CustomResult<(), errors::ProcessTrackerError> {
+    let expiry_buffer = state
+        .conf
+        .revenue_recovery
+        .recovery_timestamp
+        .redis_ttl_buffer_in_seconds;
+
+    delayed_schedule_time
+        .async_map(|t| async move {
+            let expiry_time = calculate_difference_in_seconds(t) + expiry_buffer;
+            RedisTokenManager::update_connector_customer_lock_ttl(
+                state,
+                connector_customer_id,
+                expiry_time,
+            )
+            .await
+            .change_context(errors::ProcessTrackerError::ERedisError(
+                errors::RedisError::RedisConnectionError.into(),
+            ))
+        })
+        .await
+        .transpose()?;
+
+    Ok(())
+}
+
 #[cfg(feature = "v2")]
 pub async fn get_token_with_schedule_time_based_on_retry_algorithm_type(
     state: &SessionState,
@@ -553,6 +591,13 @@ pub async fn get_token_with_schedule_time_based_on_retry_algorithm_type(
     let delayed_schedule_time =
         scheduled_time.map(|time| add_random_delay_to_schedule_time(state, time));
 
+    let _ = update_token_expiry_based_on_schedule_time(
+        state,
+        connector_customer_id,
+        delayed_schedule_time,
+    )
+    .await;
+
     Ok(delayed_schedule_time)
 }
 

## 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 -->
This PR implements an update mechanism for Redis Time-to-Live (TTL) for connector customer locks within the revenue recovery workflow.

When the decider service provides token with scheduled time, the system now updates the TTL for the corresponding customer lock in Redis. The new TTL is calculated as the difference between the schedule_time_selected_token and the current time.

The default TTL for these locks is set to 3888000 seconds (approximately 45 days). This update ensures that the lock's expiration aligns with the newly scheduled event, preventing premature lock release.

A log entry is generated to track this change, with the message: Connector customer lock TTL update with new expiry time.

Added revenue_recovery_retry_algorithm_type in business profile response. 

### 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)?
-->
For testing refer to this  : https://github.com/juspay/hyperswitch/pull/8848
System Update the Redis TTL for a connector customer lock after a best Payment Processor Token is selected.
Logs - "Connector customer lock TTL update with new expiry time"

<img width="356" height="57" alt="Screenshot 2025-09-08 at 11 15 41 AM" src="https://github.com/user-attachments/assets/13fe578a-1914-4823-b54e-94351c2b562d" />


### Business profile
#### curl 
```
curl --location --request PUT 'http://localhost:8080/v2/profiles/pro_0GHXcjcAhvGPXGu0pitK' \
--header 'x-merchant-id: cloth_seller_yMwFFEtiXtszMGELTg2P' \
--header 'Authorization: admin-api-key={}' \
--header 'Content-Type: application/json' \
--header 'api-key: test_admin' \
--data-raw '{
    "profile_name": "Update334",
    "return_url": "https://google.com/success",
    "enable_payment_response_hash": true,
    "redirect_to_merchant_with_http_post": false,
    "webhook_details": {
        "webhook_version": "1.0.1",
        "webhook_username": "ekart_retail",
        "webhook_password": "password_ekart@123",
        "webhook_url": "https://webhook.site",
        "payment_created_enabled": true,
        "payment_succeeded_enabled": true,
        "payment_failed_enabled": true
    },
    "metadata": null,
    "order_fulfillment_time": 900,
    "order_fulfillment_time_origin": "create",
    "applepay_verified_domains": null,
    "session_expiry": 900,
    "payment_link_config": null,
    "authentication_connector_details": null,
    "use_billing_as_payment_method_billing": true,
    "collect_shipping_details_from_wallet_connector_if_required": false,
    "collect_billing_details_from_wallet_connector_if_required": false,
    "always_collect_shipping_details_from_wallet_connector": false,
    "always_collect_billing_details_from_wallet_connector": false,
    "is_connector_agnostic_mit_enabled": false,
    "payout_link_config": null,
    "outgoing_webhook_custom_http_headers": null,
    "revenue_recovery_retry_algorithm_type": "smart"
}'
```

#### Response
```
{
    "merchant_id": "cloth_seller_yMwFFEtiXtszMGELTg2P",
    "id": "pro_0GHXcjcAhvGPXGu0pitK",
    "profile_name": "Update334",
    "return_url": "https://google.com/success",
    "enable_payment_response_hash": true,
    "payment_response_hash_key": "OxvXuBlTAtrqwP9VGJ9VIR1Zg7WDo1ZHEFDOxYMo4V2knbIWZA10OLE6ofW7kih8",
    "redirect_to_merchant_with_http_post": false,
    "webhook_details": {
        "webhook_version": "1.0.1",
        "webhook_username": "ekart_retail",
        "webhook_password": "password_ekart@123",
        "webhook_url": "https://webhook.site",
        "payment_created_enabled": true,
        "payment_succeeded_enabled": true,
        "payment_failed_enabled": true,
        "payment_statuses_enabled": null,
        "refund_statuses_enabled": null,
        "payout_statuses_enabled": null
    },
    "metadata": null,
    "applepay_verified_domains": null,
    "session_expiry": 900,
    "payment_link_config": null,
    "authentication_connector_details": null,
    "use_billing_as_payment_method_billing": true,
    "extended_card_info_config": null,
    "collect_shipping_details_from_wallet_connector_if_required": false,
    "collect_billing_details_from_wallet_connector_if_required": false,
    "always_collect_shipping_details_from_wallet_connector": false,
    "always_collect_billing_details_from_wallet_connector": false,
    "is_connector_agnostic_mit_enabled": false,
    "payout_link_config": null,
    "outgoing_webhook_custom_http_headers": null,
    "order_fulfillment_time": 900,
    "order_fulfillment_time_origin": "create",
    "tax_connector_id": null,
    "is_tax_connector_enabled": false,
    "is_network_tokenization_enabled": false,
    "should_collect_cvv_during_payment": null,
    "is_click_to_pay_enabled": false,
    "authentication_product_ids": null,
    "card_testing_guard_config": {
        "card_ip_blocking_status": "disabled",
        "card_ip_blocking_threshold": 3,
        "guest_user_card_blocking_status": "disabled",
        "guest_user_card_blocking_threshold": 10,
        "customer_id_blocking_status": "disabled",
        "customer_id_blocking_threshold": 5,
        "card_testing_guard_expiry": 3600
    },
    "is_clear_pan_retries_enabled": false,
    "is_debit_routing_enabled": false,
    "merchant_business_country": null,
    "is_iframe_redirection_enabled": null,
    "is_external_vault_enabled": null,
    "external_vault_connector_details": null,
    "merchant_category_code": null,
    "merchant_country_code": null,
    "split_txns_enabled": "skip",
    "revenue_recovery_retry_algorithm_type": "smart"
}
```

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