feat(revenue_recovery): Introduce hourly retry history and decision threshold in Decider Request (#10386)

Co-authored-by: Aniket Burman <aniket.burman@Aniket-Burman-JDXHW2PH34.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Aniket Burman
2025-11-26 22:04:57 +05:30
committed by GitHub
parent 0c7f82b2a2
commit 2668139fba
6 changed files with 233 additions and 97 deletions

View File

@@ -97,7 +97,7 @@ pub struct ComprehensiveCardData {
pub card_network: Option<CardNetwork>,
pub card_issuer: Option<String>,
pub card_issuing_country: Option<String>,
pub daily_retry_history: Option<HashMap<Date, i32>>,
pub daily_retry_history: Option<HashMap<PrimitiveDateTime, i32>>,
pub is_active: Option<bool>,
pub account_update_history: Option<Vec<AccountUpdateHistoryRecord>>,
}

View File

@@ -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<HashMap<Date, i32>> {
fn parse_daily_retry_history(
json_str: Option<&str>,
) -> Option<HashMap<time::PrimitiveDateTime, i32>> {
match json_str {
Some(json) if !json.is_empty() => {
match serde_json::from_str::<HashMap<String, i32>>(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);

View File

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

View File

@@ -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<String>,
/// Daily retry count history for the last 30 days (date -> retry_count)
pub daily_retry_history: HashMap<Date, i32>,
#[serde(deserialize_with = "parse_datetime_key")]
pub daily_retry_history: HashMap<PrimitiveDateTime, i32>,
/// Scheduled time for the next retry attempt
pub scheduled_at: Option<PrimitiveDateTime>,
/// Indicates if the token is a hard decline (no retries allowed)
@@ -50,6 +51,8 @@ pub struct PaymentProcessorTokenStatus {
pub is_active: Option<bool>,
/// Update history of the token
pub account_update_history: Option<Vec<AccountUpdateHistoryRecord>>,
/// Previous Decision threshold for selecting the best slot
pub decision_threshold: Option<f64>,
}
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<HashMap<PrimitiveDateTime, i32>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: HashMap<String, i32> = 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<Date, i32>,
) -> Option<(Date, i32)> {
let today = OffsetDateTime::now_utc().date();
retry_history: &HashMap<PrimitiveDateTime, i32>,
) -> 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<Date, i32> = HashMap::new();
let mut normalized_retry_history: HashMap<PrimitiveDateTime, i32> = 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<CardNetwork>,
) -> 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<bool>,
payment_processor_token_id: Option<&str>,
) -> CustomResult<bool, errors::StorageError> {
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<PrimitiveDateTime>,
decision_threshold: Option<f64>,
) -> CustomResult<bool, errors::StorageError> {
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)

View File

@@ -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<f64>,
}
#[cfg(feature = "v2")]
pub(crate) async fn get_schedule_time_for_smart_retry(
state: &SessionState,
payment_intent: &PaymentIntent,
retry_after_time: Option<prost_types::Timestamp>,
token_with_retry_info: &PaymentProcessorTokenWithRetryInfo,
) -> Result<Option<time::PrimitiveDateTime>, errors::ProcessTrackerError> {
) -> Result<Option<RetryDecision>, 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<time::PrimitiveDateTime, i32>>,
) -> HashMap<String, i32> {
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<i64>,
first_error_msg_time: Option<prost_types::Timestamp>,
wait_time: Option<prost_types::Timestamp>,
payment_id: Option<String>,
hourly_retry_history: Option<HashMap<time::PrimitiveDateTime, i32>>,
previous_threshold: Option<f64>,
}
#[cfg(feature = "v2")]
@@ -532,6 +572,11 @@ impl From<InternalDeciderRequest> 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<InternalDeciderRequest> 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<time::PrimitiveDateTime>, bool), errors::ProcessTrackerError> {
) -> Result<(Option<RetryDecision>, 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,
};
}
}

View File

@@ -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<string, int32> 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;
}