From 2668139fba8a02b8bb8ea5c48d266b00178ac13d Mon Sep 17 00:00:00 2001 From: Aniket Burman <93077964+aniketburman014@users.noreply.github.com> Date: Wed, 26 Nov 2025 22:04:57 +0530 Subject: [PATCH] feat(revenue_recovery): Introduce hourly retry history and decision threshold in Decider Request (#10386) Co-authored-by: Aniket Burman Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../src/revenue_recovery_data_backfill.rs | 2 +- .../core/revenue_recovery_data_backfill.rs | 51 +++--- .../src/core/webhooks/recovery_incoming.rs | 9 +- .../revenue_recovery_redis_operation.rs | 170 ++++++++++++------ .../router/src/workflows/revenue_recovery.rs | 94 +++++++--- proto/recovery_decider.proto | 4 + 6 files changed, 233 insertions(+), 97 deletions(-) diff --git a/crates/api_models/src/revenue_recovery_data_backfill.rs b/crates/api_models/src/revenue_recovery_data_backfill.rs index 3e5843bbe7..0148280da9 100644 --- a/crates/api_models/src/revenue_recovery_data_backfill.rs +++ b/crates/api_models/src/revenue_recovery_data_backfill.rs @@ -97,7 +97,7 @@ pub struct ComprehensiveCardData { pub card_network: Option, pub card_issuer: Option, pub card_issuing_country: Option, - pub daily_retry_history: Option>, + pub daily_retry_history: Option>, pub is_active: Option, pub account_update_history: Option>, } diff --git a/crates/router/src/core/revenue_recovery_data_backfill.rs b/crates/router/src/core/revenue_recovery_data_backfill.rs index 14beab7748..2d2b06f993 100644 --- a/crates/router/src/core/revenue_recovery_data_backfill.rs +++ b/crates/router/src/core/revenue_recovery_data_backfill.rs @@ -11,7 +11,7 @@ use error_stack::ResultExt; use hyperswitch_domain_models::api::ApplicationResponse; use masking::ExposeInterface; use router_env::{instrument, logger}; -use time::{format_description, Date}; +use time::{macros::format_description, Date}; use crate::{ connection, @@ -333,43 +333,46 @@ async fn process_payment_method_record( } /// Parse daily retry history from CSV -fn parse_daily_retry_history(json_str: Option<&str>) -> Option> { +fn parse_daily_retry_history( + json_str: Option<&str>, +) -> Option> { match json_str { Some(json) if !json.is_empty() => { match serde_json::from_str::>(json) { Ok(string_retry_history) => { - // Convert string dates to Date objects - let format = format_description::parse("[year]-[month]-[day]") - .map_err(|e| { - BackfillError::CsvParsingError(format!( - "Invalid date format configuration: {}", - e - )) - }) - .ok()?; + let date_format = format_description!("[year]-[month]-[day]"); + let datetime_format = format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]" + ); - let mut date_retry_history = HashMap::new(); + let mut hourly_retry_history = HashMap::new(); - for (date_str, count) in string_retry_history { - match Date::parse(&date_str, &format) { - Ok(date) => { - date_retry_history.insert(date, count); + for (key, count) in string_retry_history { + // Try parsing full datetime first + let parsed_dt = time::PrimitiveDateTime::parse(&key, &datetime_format) + .or_else(|_| { + // Fallback to date only + Date::parse(&key, &date_format).map(|date| { + time::PrimitiveDateTime::new(date, time::Time::MIDNIGHT) + }) + }); + + match parsed_dt { + Ok(dt) => { + hourly_retry_history.insert(dt, count); } - Err(e) => { - logger::warn!( - "Failed to parse date '{}' in daily_retry_history: {}", - date_str, - e - ); + Err(_) => { + logger::error!("Error: failed to parse retry history key '{}'", key) } } } logger::debug!( "Successfully parsed daily_retry_history with {} entries", - date_retry_history.len() + hourly_retry_history.len() ); - Some(date_retry_history) + + Some(hourly_retry_history) } Err(e) => { logger::warn!("Failed to parse daily_retry_history JSON '{}': {}", json, e); diff --git a/crates/router/src/core/webhooks/recovery_incoming.rs b/crates/router/src/core/webhooks/recovery_incoming.rs index 6efc966443..1956c9774d 100644 --- a/crates/router/src/core/webhooks/recovery_incoming.rs +++ b/crates/router/src/core/webhooks/recovery_incoming.rs @@ -929,6 +929,12 @@ impl RevenueRecoveryAttempt { .map(|category| category == common_enums::ErrorCategory::HardDecline) .unwrap_or(false); + let reference_time = time::PrimitiveDateTime::new( + recovery_attempt.created_at.date(), + time::Time::from_hms(recovery_attempt.created_at.hour(), 0, 0) + .unwrap_or(time::Time::MIDNIGHT), + ); + // Extract required fields from the revenue recovery attempt data let connector_customer_id = revenue_recovery_attempt_data.connector_customer_id.clone(); @@ -936,7 +942,7 @@ impl RevenueRecoveryAttempt { let token_unit = PaymentProcessorTokenStatus { error_code, inserted_by_attempt_id: attempt_id.clone(), - daily_retry_history: HashMap::from([(recovery_attempt.created_at.date(), 1)]), + daily_retry_history: HashMap::from([(reference_time, 1)]), scheduled_at: None, is_hard_decline: Some(is_hard_decline), modified_at: Some(recovery_attempt.created_at), @@ -960,6 +966,7 @@ impl RevenueRecoveryAttempt { }, is_active: Some(true), // Tokens created from recovery attempts are active by default account_update_history: None, // No prior account update history exists for freshly ingested tokens + decision_threshold: None, }; // Make the Redis call to store tokens 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 b4d445cff6..d8014c1f02 100644 --- a/crates/router/src/types/storage/revenue_recovery_redis_operation.rs +++ b/crates/router/src/types/storage/revenue_recovery_redis_operation.rs @@ -7,14 +7,14 @@ use error_stack::ResultExt; use masking::{ExposeInterface, PeekInterface, Secret}; use redis_interface::{DelReply, SetnxReply}; use router_env::{instrument, logger, tracing}; -use serde::{Deserialize, Serialize}; -use time::{Date, Duration, OffsetDateTime, PrimitiveDateTime}; +use serde::{Deserialize, Deserializer, Serialize}; +use time::{Date, Duration, OffsetDateTime, PrimitiveDateTime, Time}; use crate::{db::errors, types::storage::enums::RevenueRecoveryAlgorithmType, SessionState}; // Constants for retry window management -const RETRY_WINDOW_DAYS: i32 = 30; const INITIAL_RETRY_COUNT: i32 = 0; +const RETRY_WINDOW_IN_HOUR: i32 = 720; /// Payment processor token details including card information #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)] @@ -39,7 +39,8 @@ pub struct PaymentProcessorTokenStatus { /// Error code associated with the token failure pub error_code: Option, /// Daily retry count history for the last 30 days (date -> retry_count) - pub daily_retry_history: HashMap, + #[serde(deserialize_with = "parse_datetime_key")] + pub daily_retry_history: HashMap, /// Scheduled time for the next retry attempt pub scheduled_at: Option, /// Indicates if the token is a hard decline (no retries allowed) @@ -50,6 +51,8 @@ pub struct PaymentProcessorTokenStatus { pub is_active: Option, /// Update history of the token pub account_update_history: Option>, + /// Previous Decision threshold for selecting the best slot + pub decision_threshold: Option, } impl From<&PaymentProcessorTokenDetails> for api_models::payments::AdditionalCardInfo { @@ -74,6 +77,34 @@ impl From<&PaymentProcessorTokenDetails> for api_models::payments::AdditionalCar } } +fn parse_datetime_key<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let raw: HashMap = HashMap::deserialize(deserializer)?; + let mut parsed = HashMap::new(); + + // Full datetime + let full_dt_format = time::macros::format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]" + ); + // Date only + let date_only_format = time::macros::format_description!("[year]-[month]-[day]"); + + for (k, v) in raw { + let dt = PrimitiveDateTime::parse(&k, &full_dt_format) + .or_else(|_| { + Date::parse(&k, &date_only_format) + .map(|date| PrimitiveDateTime::new(date, Time::MIDNIGHT)) + }) + .map_err(|_| serde::de::Error::custom(format!("Invalid date key: {}", k)))?; + + parsed.insert(dt, v); + } + + Ok(parsed) +} + /// Token retry availability information with detailed wait times #[derive(Debug, Clone)] pub struct TokenRetryInfo { @@ -299,13 +330,17 @@ impl RedisTokenManager { /// Find the most recent date from retry history pub fn find_nearest_date_from_current( - retry_history: &HashMap, - ) -> Option<(Date, i32)> { - let today = OffsetDateTime::now_utc().date(); + retry_history: &HashMap, + ) -> Option<(PrimitiveDateTime, i32)> { + let now_utc = OffsetDateTime::now_utc(); + let reference_time = PrimitiveDateTime::new( + now_utc.date(), + Time::from_hms(now_utc.hour(), 0, 0).unwrap_or(Time::MIDNIGHT), + ); retry_history .iter() - .filter(|(date, _)| **date <= today) // Only past dates + today + .filter(|(date, _)| **date <= reference_time) // Only past dates + today .max_by_key(|(date, _)| *date) // Get the most recent .map(|(date, retry_count)| (*date, *retry_count)) } @@ -362,24 +397,15 @@ impl RedisTokenManager { Ok(()) } - /// Get current date in `yyyy-mm-dd` format. - pub fn get_current_date() -> String { - let today = date_time::now().date(); - - let (year, month, day) = (today.year(), today.month(), today.day()); - - format!("{year:04}-{month:02}-{day:02}",) - } - /// Normalize retry window to exactly `RETRY_WINDOW_DAYS` days (today to `RETRY_WINDOW_DAYS - 1` days ago). pub fn normalize_retry_window( payment_processor_token: &mut PaymentProcessorTokenStatus, - today: Date, + reference_time: PrimitiveDateTime, ) { - let mut normalized_retry_history: HashMap = HashMap::new(); + let mut normalized_retry_history: HashMap = HashMap::new(); - for days_ago in 0..RETRY_WINDOW_DAYS { - let date = today - Duration::days(days_ago.into()); + for hours_ago in 0..RETRY_WINDOW_IN_HOUR { + let date = reference_time - Duration::hours(hours_ago.into()); payment_processor_token .daily_retry_history @@ -415,7 +441,6 @@ impl RedisTokenManager { let retry_info = Self::payment_processor_token_retry_info( state, payment_processor_token_status, - today, card_network.clone(), ); @@ -449,13 +474,17 @@ impl RedisTokenManager { } /// Sum retries over exactly the last 30 days - fn calculate_total_30_day_retries(token: &PaymentProcessorTokenStatus, today: Date) -> i32 { - (0..RETRY_WINDOW_DAYS) + fn calculate_total_30_day_retries( + token: &PaymentProcessorTokenStatus, + reference_time: PrimitiveDateTime, + ) -> i32 { + (0..RETRY_WINDOW_IN_HOUR) .map(|i| { - let date = today - Duration::days(i.into()); + let target_hour = reference_time - Duration::hours(i.into()); + token .daily_retry_history - .get(&date) + .get(&target_hour) .copied() .unwrap_or(INITIAL_RETRY_COUNT) }) @@ -463,53 +492,76 @@ impl RedisTokenManager { } /// Calculate wait hours - fn calculate_wait_hours(target_date: Date, now: OffsetDateTime) -> i64 { - let expiry_time = target_date.midnight().assume_utc(); + fn calculate_wait_hours(target_date: PrimitiveDateTime, now: OffsetDateTime) -> i64 { + let expiry_time = target_date.assume_utc(); (expiry_time - now).whole_hours().max(0) } - /// Calculate retry counts for exactly the last 30 days + /// Calculate retry counts for exactly the last 30 days (hour-granular) pub fn payment_processor_token_retry_info( state: &SessionState, token: &PaymentProcessorTokenStatus, - today: Date, network_type: Option, ) -> TokenRetryInfo { let card_config = &state.conf.revenue_recovery.card_config; let card_network_config = card_config.get_network_config(network_type); - let now = OffsetDateTime::now_utc(); + let now_utc = OffsetDateTime::now_utc(); + let reference_time = PrimitiveDateTime::new( + now_utc.date(), + Time::from_hms(now_utc.hour(), 0, 0).unwrap_or(Time::MIDNIGHT), + ); - let total_30_day_retries = Self::calculate_total_30_day_retries(token, today); + // Total retries for last 720 hours + let total_30_day_retries = Self::calculate_total_30_day_retries(token, reference_time); + // Monthly wait-hour calculation ---- let monthly_wait_hours = if total_30_day_retries >= card_network_config.max_retry_count_for_thirty_day { let mut accumulated_retries = 0; - // Iterate from most recent to oldest - (0..RETRY_WINDOW_DAYS) - .map(|days_ago| today - Duration::days(days_ago.into())) - .find(|date| { - let retries = token.daily_retry_history.get(date).copied().unwrap_or(0); + (0..RETRY_WINDOW_IN_HOUR) + .map(|i| reference_time - Duration::hours(i.into())) + .find(|window_hour| { + let retries = token + .daily_retry_history + .get(window_hour) + .copied() + .unwrap_or(0); accumulated_retries += retries; + accumulated_retries >= card_network_config.max_retry_count_for_thirty_day }) - .map(|breach_date| { - Self::calculate_wait_hours(breach_date + Duration::days(31), now) + .map(|breach_hour| { + let allowed_at = breach_hour + Duration::days(31); + Self::calculate_wait_hours(allowed_at, now_utc) }) .unwrap_or(0) } else { 0 }; - let today_retries = token - .daily_retry_history - .get(&today) - .copied() - .unwrap_or(INITIAL_RETRY_COUNT); + // Today's retries (using hourly buckets) ---- + let today_date = reference_time.date(); + + let today_retries: i32 = (0..24) + .map(|h| { + let hour_bucket = PrimitiveDateTime::new( + today_date, + Time::from_hms(h, 0, 0).unwrap_or(Time::MIDNIGHT), + ); + token + .daily_retry_history + .get(&hour_bucket) + .copied() + .unwrap_or(0) + }) + .sum(); let daily_wait_hours = if today_retries >= card_network_config.max_retries_per_day { - Self::calculate_wait_hours(today + Duration::days(1), now) + let tomorrow_start = + PrimitiveDateTime::new(today_date + Duration::days(1), Time::MIDNIGHT); + Self::calculate_wait_hours(tomorrow_start, now_utc) } else { 0 }; @@ -542,12 +594,16 @@ impl RedisTokenManager { let last_external_attempt_at = token_data.modified_at; - let today = OffsetDateTime::now_utc().date(); + let now_utc = OffsetDateTime::now_utc(); + let reference_time = PrimitiveDateTime::new( + now_utc.date(), + Time::from_hms(now_utc.hour(), 0, 0).unwrap_or(Time::MIDNIGHT), + ); token_map .get_mut(&token_id) .map(|existing_token| { - Self::normalize_retry_window(existing_token, today); + Self::normalize_retry_window(existing_token, reference_time); for (date, &value) in &token_data.daily_retry_history { existing_token @@ -611,7 +667,11 @@ impl RedisTokenManager { is_hard_decline: &Option, payment_processor_token_id: Option<&str>, ) -> CustomResult { - let today = OffsetDateTime::now_utc().date(); + let now_utc = OffsetDateTime::now_utc(); + let reference_time = PrimitiveDateTime::new( + now_utc.date(), + Time::from_hms(now_utc.hour(), 0, 0).unwrap_or(Time::MIDNIGHT), + ); let updated_token = match payment_processor_token_id { Some(token_id) => { Self::get_connector_customer_payment_processor_tokens(state, connector_customer_id) @@ -638,6 +698,7 @@ impl RedisTokenManager { )), is_active: status.is_active, account_update_history: status.account_update_history.clone(), + decision_threshold: status.decision_threshold, }) } None => None, @@ -645,17 +706,19 @@ impl RedisTokenManager { match updated_token { Some(mut token) => { - Self::normalize_retry_window(&mut token, today); + Self::normalize_retry_window(&mut token, reference_time); match token.error_code { None => token.daily_retry_history.clear(), Some(_) => { let current_count = token .daily_retry_history - .get(&today) + .get(&reference_time) .copied() .unwrap_or(INITIAL_RETRY_COUNT); - token.daily_retry_history.insert(today, current_count + 1); + token + .daily_retry_history + .insert(reference_time, current_count + 1); } } @@ -716,6 +779,7 @@ impl RedisTokenManager { )), is_active: status.is_active, account_update_history: status.account_update_history.clone(), + decision_threshold: status.decision_threshold, }; updated_tokens_map.insert(token_id, updated_status); } @@ -742,6 +806,7 @@ impl RedisTokenManager { connector_customer_id: &str, payment_processor_token: &str, schedule_time: Option, + decision_threshold: Option, ) -> CustomResult { let updated_token = Self::get_connector_customer_payment_processor_tokens(state, connector_customer_id) @@ -766,6 +831,7 @@ impl RedisTokenManager { )), is_active: status.is_active, account_update_history: status.account_update_history.clone(), + decision_threshold: decision_threshold.or(status.decision_threshold), }); match updated_token { @@ -918,6 +984,7 @@ impl RedisTokenManager { connector_customer_id, &t.payment_processor_token_details.payment_processor_token, None, + None, ) .await?; @@ -1307,6 +1374,7 @@ impl AccountUpdaterAction { updated_mandate_details, )), }]), + decision_threshold: None, }; RedisTokenManager::upsert_payment_processor_token(state, customer_id, new_token) diff --git a/crates/router/src/workflows/revenue_recovery.rs b/crates/router/src/workflows/revenue_recovery.rs index 8d499caa0a..8a52830876 100644 --- a/crates/router/src/workflows/revenue_recovery.rs +++ b/crates/router/src/workflows/revenue_recovery.rs @@ -271,13 +271,19 @@ pub(crate) async fn get_schedule_time_to_retry_mit_payments( scheduler_utils::get_time_from_delta(time_delta) } +#[derive(Debug, Clone)] +pub struct RetryDecision { + pub retry_time: time::PrimitiveDateTime, + pub decision_threshold: Option, +} + #[cfg(feature = "v2")] pub(crate) async fn get_schedule_time_for_smart_retry( state: &SessionState, payment_intent: &PaymentIntent, retry_after_time: Option, token_with_retry_info: &PaymentProcessorTokenWithRetryInfo, -) -> Result, errors::ProcessTrackerError> { +) -> Result, errors::ProcessTrackerError> { let card_config = &state.conf.revenue_recovery.card_config; // Not populating it right now @@ -395,6 +401,14 @@ pub(crate) async fn get_schedule_time_for_smart_retry( ), first_error_msg_time: None, wait_time: retry_after_time, + payment_id: Some(payment_intent.get_id().get_string_repr().to_string()), + hourly_retry_history: Some( + token_with_retry_info + .token_status + .daily_retry_history + .clone(), + ), + previous_threshold: token_with_retry_info.token_status.decision_threshold, }; if let Some(mut client) = state.grpc_client.recovery_decider_client.clone() { @@ -408,7 +422,13 @@ pub(crate) async fn get_schedule_time_for_smart_retry( .and(grpc_response.retry_time) .and_then(|prost_ts| { match date_time::convert_from_prost_timestamp(&prost_ts) { - Ok(pdt) => Some(pdt), + Ok(pdt) => { + let response = RetryDecision { + retry_time: pdt, + decision_threshold: grpc_response.decision_threshold, + }; + Some(response) + } Err(e) => { logger::error!( "Failed to convert retry_time from prost::Timestamp: {e:?}" @@ -458,13 +478,30 @@ async fn should_force_schedule_due_to_missed_slots( .max_retry_count_for_thirty_day; // Calculate time difference since last retry and compare with threshold - (time::OffsetDateTime::now_utc() - most_recent_date.midnight().assume_utc()).whole_hours() + (time::OffsetDateTime::now_utc() - most_recent_date.assume_utc()).whole_hours() > threshold_hours.into() }) // Default to false if no valid retry history found (either none exists or all have retry_count = 0) .unwrap_or(false)) } +#[cfg(feature = "v2")] +pub fn convert_hourly_retry_history( + input: Option>, +) -> HashMap { + let fmt = time::macros::format_description!( + "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]" + ); + + match input { + Some(map) => map + .into_iter() + .map(|(dt, count)| (dt.format(&fmt).unwrap_or(dt.to_string()), count)) + .collect(), + None => HashMap::new(), + } +} + #[cfg(feature = "v2")] #[derive(Debug)] struct InternalDeciderRequest { @@ -497,6 +534,9 @@ struct InternalDeciderRequest { total_retry_count_within_network: Option, first_error_msg_time: Option, wait_time: Option, + payment_id: Option, + hourly_retry_history: Option>, + previous_threshold: Option, } #[cfg(feature = "v2")] @@ -532,6 +572,11 @@ impl From for external_grpc_client::DeciderRequest { total_retry_count_within_network: internal_request.total_retry_count_within_network, first_error_msg_time: internal_request.first_error_msg_time, wait_time: internal_request.wait_time, + payment_id: internal_request.payment_id, + hourly_retry_history: convert_hourly_retry_history( + internal_request.hourly_retry_history, + ), + previous_threshold: internal_request.previous_threshold, } } } @@ -540,7 +585,7 @@ impl From for external_grpc_client::DeciderRequest { #[derive(Debug, Clone)] pub struct ScheduledToken { pub token_details: PaymentProcessorTokenDetails, - pub schedule_time: time::PrimitiveDateTime, + pub retry_decision: RetryDecision, } #[cfg(feature = "v2")] @@ -831,7 +876,7 @@ pub async fn calculate_smart_retry_time( state: &SessionState, payment_intent: &PaymentIntent, token_with_retry_info: &PaymentProcessorTokenWithRetryInfo, -) -> Result<(Option, bool), errors::ProcessTrackerError> { +) -> Result<(Option, bool), errors::ProcessTrackerError> { let wait_hours = token_with_retry_info.retry_wait_time_hours; let current_time = time::OffsetDateTime::now_utc(); let future_time = current_time + time::Duration::hours(wait_hours); @@ -875,16 +920,20 @@ pub async fn calculate_smart_retry_time( scheduled_time ); return Ok(( - Some(time::PrimitiveDateTime::new( - scheduled_time.date(), - scheduled_time.time(), - )), - true, - )); // force_scheduled = true + Some(RetryDecision { + retry_time: time::PrimitiveDateTime::new( + scheduled_time.date(), + scheduled_time.time(), + ), + // Not populating decision_threshold in forced schedule as there is no decider call + decision_threshold: None, + }), + true, // force_scheduled + )); } // Normal smart retry path - let schedule_time = get_schedule_time_for_smart_retry( + let retry_decision = get_schedule_time_for_smart_retry( state, payment_intent, future_timestamp, @@ -892,7 +941,7 @@ pub async fn calculate_smart_retry_time( ) .await?; - Ok((schedule_time, false)) // force_scheduled = false + Ok((retry_decision, false)) // force_scheduled = false } #[cfg(feature = "v2")] @@ -918,13 +967,13 @@ async fn process_token_for_retry( }) } false => { - let (schedule_time, force_scheduled) = + let (retry_decision, force_scheduled) = calculate_smart_retry_time(state, payment_intent, token_with_retry_info).await?; Ok(TokenProcessResult { - scheduled_token: schedule_time.map(|schedule_time| ScheduledToken { + scheduled_token: retry_decision.map(|retry_decision| ScheduledToken { token_details: token_status.payment_processor_token_details.clone(), - schedule_time, + retry_decision, }), force_scheduled, }) @@ -960,7 +1009,11 @@ pub async fn call_decider_for_payment_processor_tokens_select_closest_time( tokens_with_schedule_time = vec![ScheduledToken { token_details: token_details.clone(), - schedule_time, + retry_decision: RetryDecision { + retry_time: schedule_time, + // Not populating decision_threshold for successful token as there is no decider call + decision_threshold: None, + }, }]; tracing::debug!( @@ -995,7 +1048,7 @@ pub async fn call_decider_for_payment_processor_tokens_select_closest_time( let best_token = tokens_with_schedule_time .iter() - .min_by_key(|token| token.schedule_time) + .min_by_key(|token| token.retry_decision.retry_time) .cloned(); let mut payment_processor_token_response; @@ -1040,13 +1093,14 @@ pub async fn call_decider_for_payment_processor_tokens_select_closest_time( state, connector_customer_id, &token.token_details.payment_processor_token, - Some(token.schedule_time), + Some(token.retry_decision.retry_time), + token.retry_decision.decision_threshold, ) .await .change_context(errors::ProcessTrackerError::EApiErrorResponse)?; payment_processor_token_response = PaymentProcessorTokenResponse::ScheduledTime { - scheduled_time: token.schedule_time, + scheduled_time: token.retry_decision.retry_time, }; } } diff --git a/proto/recovery_decider.proto b/proto/recovery_decider.proto index b6346bc7ba..34787c636b 100644 --- a/proto/recovery_decider.proto +++ b/proto/recovery_decider.proto @@ -38,9 +38,13 @@ message DeciderRequest { optional int64 total_retry_count_within_network = 27; optional google.protobuf.Timestamp first_error_msg_time = 28; optional google.protobuf.Timestamp wait_time = 29; + optional string payment_id= 30; + map hourly_retry_history = 31; + optional double previous_threshold= 32; } message DeciderResponse { bool retry_flag = 1; google.protobuf.Timestamp retry_time = 2; + optional double decision_threshold= 3; }