feat(process_tracker): make long standing payments failed (#2380)

Co-authored-by: Arun Raj M <jarnura47@gmail.com>
This commit is contained in:
Narayan Bhat
2023-10-09 14:32:46 +05:30
committed by GitHub
parent 17393f5be3
commit 73dfc31f9d
7 changed files with 271 additions and 86 deletions

View File

@ -13,6 +13,8 @@ pub(crate) const ALPHABETS: [char; 62] = [
pub const REQUEST_TIME_OUT: u64 = 30; pub const REQUEST_TIME_OUT: u64 = 30;
pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT"; pub const REQUEST_TIMEOUT_ERROR_CODE: &str = "TIMEOUT";
pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time"; pub const REQUEST_TIMEOUT_ERROR_MESSAGE: &str = "Connector did not respond in specified time";
pub const REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC: &str =
"This Payment has been moved to failed as there is no response from the connector";
///Payment intent fulfillment default timeout (in seconds) ///Payment intent fulfillment default timeout (in seconds)
pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60; pub const DEFAULT_FULFILLMENT_TIME: i64 = 15 * 60;

View File

@ -2,12 +2,11 @@ use std::{fmt::Debug, marker::PhantomData, str::FromStr};
use api_models::payments::FrmMessage; use api_models::payments::FrmMessage;
use common_utils::fp_utils; use common_utils::fp_utils;
use data_models::mandates::MandateData;
use diesel_models::ephemeral_key; use diesel_models::ephemeral_key;
use error_stack::{IntoReport, ResultExt}; use error_stack::{IntoReport, ResultExt};
use router_env::{instrument, tracing}; use router_env::{instrument, tracing};
use super::{flows::Feature, PaymentAddress, PaymentData}; use super::{flows::Feature, PaymentData};
use crate::{ use crate::{
configs::settings::{ConnectorRequestReferenceIdConfig, Server}, configs::settings::{ConnectorRequestReferenceIdConfig, Server},
connector::Nexinets, connector::Nexinets,
@ -202,40 +201,32 @@ where
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
connector_http_status_code: Option<u16>, connector_http_status_code: Option<u16>,
) -> RouterResponse<Self> { ) -> RouterResponse<Self> {
let captures = payment_data let captures =
.multiple_capture_data payment_data
.and_then(|multiple_capture_data| { .multiple_capture_data
multiple_capture_data .clone()
.expand_captures .and_then(|multiple_capture_data| {
.and_then(|should_expand| { multiple_capture_data
should_expand.then_some( .expand_captures
multiple_capture_data .and_then(|should_expand| {
.get_all_captures() should_expand.then_some(
.into_iter() multiple_capture_data
.cloned() .get_all_captures()
.collect(), .into_iter()
) .cloned()
}) .collect(),
}); )
})
});
payments_to_payments_response( payments_to_payments_response(
req, req,
payment_data.payment_attempt, payment_data,
payment_data.payment_intent,
payment_data.refunds,
payment_data.disputes,
payment_data.attempts,
captures, captures,
payment_data.payment_method_data,
customer, customer,
auth_flow, auth_flow,
payment_data.address,
server, server,
payment_data.connector_response.authentication_data,
&operation, &operation,
payment_data.ephemeral_key,
payment_data.sessions_token,
payment_data.frm_message,
payment_data.setup_mandate,
connector_request_reference_id_config, connector_request_reference_id_config,
connector_http_status_code, connector_http_status_code,
) )
@ -333,31 +324,23 @@ where
// try to use router data here so that already validated things , we don't want to repeat the validations. // try to use router data here so that already validated things , we don't want to repeat the validations.
// Add internal value not found and external value not found so that we can give 500 / Internal server error for internal value not found // Add internal value not found and external value not found so that we can give 500 / Internal server error for internal value not found
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn payments_to_payments_response<R, Op>( pub fn payments_to_payments_response<R, Op, F: Clone>(
payment_request: Option<R>, payment_request: Option<R>,
payment_attempt: storage::PaymentAttempt, payment_data: PaymentData<F>,
payment_intent: storage::PaymentIntent,
refunds: Vec<storage::Refund>,
disputes: Vec<storage::Dispute>,
option_attempts: Option<Vec<storage::PaymentAttempt>>,
captures: Option<Vec<storage::Capture>>, captures: Option<Vec<storage::Capture>>,
payment_method_data: Option<api::PaymentMethodData>,
customer: Option<domain::Customer>, customer: Option<domain::Customer>,
auth_flow: services::AuthFlow, auth_flow: services::AuthFlow,
address: PaymentAddress,
server: &Server, server: &Server,
redirection_data: Option<serde_json::Value>,
operation: &Op, operation: &Op,
ephemeral_key_option: Option<ephemeral_key::EphemeralKey>,
session_tokens: Vec<api::SessionToken>,
fraud_check: Option<payments::FraudCheck>,
mandate_data: Option<MandateData>,
connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig, connector_request_reference_id_config: &ConnectorRequestReferenceIdConfig,
connector_http_status_code: Option<u16>, connector_http_status_code: Option<u16>,
) -> RouterResponse<api::PaymentsResponse> ) -> RouterResponse<api::PaymentsResponse>
where where
Op: Debug, Op: Debug,
{ {
let payment_attempt = payment_data.payment_attempt;
let payment_intent = payment_data.payment_intent;
let currency = payment_attempt let currency = payment_attempt
.currency .currency
.as_ref() .as_ref()
@ -369,22 +352,31 @@ where
field_name: "amount", field_name: "amount",
})?; })?;
let mandate_id = payment_attempt.mandate_id.clone(); let mandate_id = payment_attempt.mandate_id.clone();
let refunds_response = if refunds.is_empty() { let refunds_response = if payment_data.refunds.is_empty() {
None
} else {
Some(refunds.into_iter().map(ForeignInto::foreign_into).collect())
};
let disputes_response = if disputes.is_empty() {
None None
} else { } else {
Some( Some(
disputes payment_data
.refunds
.into_iter() .into_iter()
.map(ForeignInto::foreign_into) .map(ForeignInto::foreign_into)
.collect(), .collect(),
) )
}; };
let attempts_response = option_attempts.map(|attempts| {
let disputes_response = if payment_data.disputes.is_empty() {
None
} else {
Some(
payment_data
.disputes
.into_iter()
.map(ForeignInto::foreign_into)
.collect(),
)
};
let attempts_response = payment_data.attempts.map(|attempts| {
attempts attempts
.into_iter() .into_iter()
.map(ForeignInto::foreign_into) .map(ForeignInto::foreign_into)
@ -419,7 +411,7 @@ where
field_name: "payment_method_data", field_name: "payment_method_data",
})?; })?;
let merchant_decision = payment_intent.merchant_decision.to_owned(); let merchant_decision = payment_intent.merchant_decision.to_owned();
let frm_message = fraud_check.map(FrmMessage::foreign_from); let frm_message = payment_data.frm_message.map(FrmMessage::foreign_from);
let payment_method_data_response = let payment_method_data_response =
additional_payment_method_data.map(api::PaymentMethodDataResponse::from); additional_payment_method_data.map(api::PaymentMethodDataResponse::from);
@ -441,13 +433,23 @@ where
let output = Ok(match payment_request { let output = Ok(match payment_request {
Some(_request) => { Some(_request) => {
if payments::is_start_pay(&operation) && redirection_data.is_some() { if payments::is_start_pay(&operation)
let redirection_data = redirection_data.get_required_value("redirection_data")?; && payment_data
.connector_response
.authentication_data
.is_some()
{
let redirection_data = payment_data
.connector_response
.authentication_data
.get_required_value("redirection_data")?;
let form: RedirectForm = serde_json::from_value(redirection_data) let form: RedirectForm = serde_json::from_value(redirection_data)
.map_err(|_| errors::ApiErrorResponse::InternalServerError)?; .map_err(|_| errors::ApiErrorResponse::InternalServerError)?;
services::ApplicationResponse::Form(Box::new(services::RedirectionFormData { services::ApplicationResponse::Form(Box::new(services::RedirectionFormData {
redirect_form: form, redirect_form: form,
payment_method_data, payment_method_data: payment_data.payment_method_data,
amount, amount,
currency: currency.to_string(), currency: currency.to_string(),
})) }))
@ -494,22 +496,23 @@ where
display_to_timestamp: wait_screen_data.display_to_timestamp, display_to_timestamp: wait_screen_data.display_to_timestamp,
} }
})) }))
.or(redirection_data.map(|_| { .or(payment_data
api_models::payments::NextActionData::RedirectToUrl { .connector_response
.authentication_data
.map(|_| api_models::payments::NextActionData::RedirectToUrl {
redirect_to_url: helpers::create_startpay_url( redirect_to_url: helpers::create_startpay_url(
server, server,
&payment_attempt, &payment_attempt,
&payment_intent, &payment_intent,
), ),
} }));
}));
}; };
// next action check for third party sdk session (for ex: Apple pay through trustpay has third party sdk session response) // next action check for third party sdk session (for ex: Apple pay through trustpay has third party sdk session response)
if third_party_sdk_session_next_action(&payment_attempt, operation) { if third_party_sdk_session_next_action(&payment_attempt, operation) {
next_action_response = Some( next_action_response = Some(
api_models::payments::NextActionData::ThirdPartySdkSessionToken { api_models::payments::NextActionData::ThirdPartySdkSessionToken {
session_token: session_tokens.get(0).cloned(), session_token: payment_data.sessions_token.get(0).cloned(),
}, },
) )
} }
@ -555,7 +558,7 @@ where
) )
.set_mandate_id(mandate_id) .set_mandate_id(mandate_id)
.set_mandate_data( .set_mandate_data(
mandate_data.map(|d| api::MandateData { payment_data.setup_mandate.map(|d| api::MandateData {
customer_acceptance: d.customer_acceptance.map(|d| { customer_acceptance: d.customer_acceptance.map(|d| {
api::CustomerAcceptance { api::CustomerAcceptance {
acceptance_type: match d.acceptance_type { acceptance_type: match d.acceptance_type {
@ -621,8 +624,8 @@ where
.or(payment_attempt.error_message), .or(payment_attempt.error_message),
) )
.set_error_code(payment_attempt.error_code) .set_error_code(payment_attempt.error_code)
.set_shipping(address.shipping) .set_shipping(payment_data.address.shipping)
.set_billing(address.billing) .set_billing(payment_data.address.billing)
.set_next_action(next_action_response) .set_next_action(next_action_response)
.set_return_url(payment_intent.return_url) .set_return_url(payment_intent.return_url)
.set_cancellation_reason(payment_attempt.cancellation_reason) .set_cancellation_reason(payment_attempt.cancellation_reason)
@ -642,7 +645,9 @@ where
.set_allowed_payment_method_types( .set_allowed_payment_method_types(
payment_intent.allowed_payment_method_types, payment_intent.allowed_payment_method_types,
) )
.set_ephemeral_key(ephemeral_key_option.map(ForeignFrom::foreign_from)) .set_ephemeral_key(
payment_data.ephemeral_key.map(ForeignFrom::foreign_from),
)
.set_frm_message(frm_message) .set_frm_message(frm_message)
.set_merchant_decision(merchant_decision) .set_merchant_decision(merchant_decision)
.set_manual_retry_allowed(helpers::is_manual_retry_allowed( .set_manual_retry_allowed(helpers::is_manual_retry_allowed(
@ -696,8 +701,8 @@ where
.as_ref() .as_ref()
.and_then(|cus| cus.phone.as_ref().map(|s| s.to_owned())), .and_then(|cus| cus.phone.as_ref().map(|s| s.to_owned())),
mandate_id, mandate_id,
shipping: address.shipping, shipping: payment_data.address.shipping,
billing: address.billing, billing: payment_data.address.billing,
cancellation_reason: payment_attempt.cancellation_reason, cancellation_reason: payment_attempt.cancellation_reason,
payment_token: payment_attempt.payment_token, payment_token: payment_attempt.payment_token,
metadata: payment_intent.metadata, metadata: payment_intent.metadata,

View File

@ -861,13 +861,13 @@ pub async fn sync_refund_with_gateway_workflow(
.await? .await?
} }
_ => { _ => {
payment_sync::retry_sync_task( _ = payment_sync::retry_sync_task(
&*state.store, &*state.store,
response.connector, response.connector,
response.merchant_id, response.merchant_id,
refund_tracker.to_owned(), refund_tracker.to_owned(),
) )
.await? .await?;
} }
} }

View File

@ -695,11 +695,6 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: types::OutgoingWebhook
}?; }?;
if state.conf.webhooks.outgoing_enabled { if state.conf.webhooks.outgoing_enabled {
let arbiter = actix::Arbiter::try_current()
.ok_or(errors::ApiErrorResponse::WebhookProcessingFailure)
.into_report()
.attach_printable("arbiter retrieval failure")?;
let outgoing_webhook = api::OutgoingWebhook { let outgoing_webhook = api::OutgoingWebhook {
merchant_id: merchant_account.merchant_id.clone(), merchant_id: merchant_account.merchant_id.clone(),
event_id: event.event_id, event_id: event.event_id,
@ -708,7 +703,9 @@ pub async fn create_event_and_trigger_outgoing_webhook<W: types::OutgoingWebhook
timestamp: event.created_at, timestamp: event.created_at,
}; };
arbiter.spawn(async move { // Using a tokio spawn here and not arbiter because not all caller of this function
// may have an actix arbiter
tokio::spawn(async move {
let result = let result =
trigger_webhook_to_merchant::<W>(merchant_account, outgoing_webhook, &state).await; trigger_webhook_to_merchant::<W>(merchant_account, outgoing_webhook, &state).await;

View File

@ -5,6 +5,8 @@ pub mod ext_traits;
#[cfg(feature = "kv_store")] #[cfg(feature = "kv_store")]
pub mod storage_partitioning; pub mod storage_partitioning;
use std::fmt::Debug;
use api_models::{enums, payments, webhooks}; use api_models::{enums, payments, webhooks};
use base64::Engine; use base64::Engine;
pub use common_utils::{ pub use common_utils::{
@ -27,11 +29,12 @@ use crate::{
consts, consts,
core::{ core::{
errors::{self, CustomResult, RouterResult, StorageErrorExt}, errors::{self, CustomResult, RouterResult, StorageErrorExt},
utils, utils, webhooks as webhooks_core,
}, },
db::StorageInterface, db::StorageInterface,
logger, logger,
routes::metrics, routes::metrics,
services,
types::{ types::{
self, self,
domain::{ domain::{
@ -39,6 +42,7 @@ use crate::{
types::{encrypt_optional, AsyncLift}, types::{encrypt_optional, AsyncLift},
}, },
storage, storage,
transformers::{ForeignTryFrom, ForeignTryInto},
}, },
}; };
@ -669,3 +673,89 @@ pub fn add_apple_pay_payment_status_metrics(
} }
} }
} }
impl ForeignTryFrom<enums::IntentStatus> for enums::EventType {
type Error = errors::ValidationError;
fn foreign_try_from(value: enums::IntentStatus) -> Result<Self, Self::Error> {
match value {
enums::IntentStatus::Succeeded => Ok(Self::PaymentSucceeded),
enums::IntentStatus::Failed => Ok(Self::PaymentFailed),
enums::IntentStatus::Processing => Ok(Self::PaymentProcessing),
enums::IntentStatus::RequiresMerchantAction
| enums::IntentStatus::RequiresCustomerAction => Ok(Self::ActionRequired),
_ => Err(errors::ValidationError::IncorrectValueProvided {
field_name: "intent_status",
}),
}
}
}
pub async fn trigger_payments_webhook<F, Req, Op>(
merchant_account: domain::MerchantAccount,
payment_data: crate::core::payments::PaymentData<F>,
req: Option<Req>,
customer: Option<domain::Customer>,
state: &crate::routes::AppState,
operation: Op,
) -> RouterResult<()>
where
F: Send + Clone + Sync,
Op: Debug,
{
let status = payment_data.payment_intent.status;
let payment_id = payment_data.payment_intent.payment_id.clone();
let captures = payment_data
.multiple_capture_data
.clone()
.map(|multiple_capture_data| {
multiple_capture_data
.get_all_captures()
.into_iter()
.cloned()
.collect()
});
if matches!(
status,
enums::IntentStatus::Succeeded | enums::IntentStatus::Failed
) {
let payments_response = crate::core::payments::transformers::payments_to_payments_response(
req,
payment_data,
captures,
customer,
services::AuthFlow::Merchant,
&state.conf.server,
&operation,
&state.conf.connector_request_reference_id_config,
None,
)?;
let event_type: enums::EventType = status
.foreign_try_into()
.into_report()
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("payment event type mapping failed")?;
if let services::ApplicationResponse::JsonWithHeaders((payments_response_json, _)) =
payments_response
{
Box::pin(
webhooks_core::create_event_and_trigger_appropriate_outgoing_webhook(
state.clone(),
merchant_account,
event_type,
diesel_models::enums::EventClass::Payments,
None,
payment_id,
diesel_models::enums::EventObjectType::PaymentDetails,
webhooks::OutgoingWebhookContent::PaymentDetails(payments_response_json),
),
)
.await?;
}
}
Ok(())
}

View File

@ -4,11 +4,13 @@ use router_env::logger;
use scheduler::{ use scheduler::{
consumer::{self, types::process_data, workflows::ProcessTrackerWorkflow}, consumer::{self, types::process_data, workflows::ProcessTrackerWorkflow},
db::process_tracker::ProcessTrackerExt, db::process_tracker::ProcessTrackerExt,
errors as sch_errors, utils, SchedulerAppState, errors as sch_errors, utils as scheduler_utils, SchedulerAppState,
}; };
use crate::{ use crate::{
consts,
core::{ core::{
errors::StorageErrorExt,
payment_methods::Oss, payment_methods::Oss,
payments::{self as payment_flows, operations}, payments::{self as payment_flows, operations},
}, },
@ -20,6 +22,7 @@ use crate::{
api, api,
storage::{self, enums}, storage::{self, enums},
}, },
utils,
}; };
pub struct PaymentsSyncWorkflow; pub struct PaymentsSyncWorkflow;
@ -57,7 +60,7 @@ impl ProcessTrackerWorkflow<AppState> for PaymentsSyncWorkflow {
) )
.await?; .await?;
let (payment_data, _, _, _) = let (mut payment_data, _, customer, _) =
payment_flows::payments_operation_core::<api::PSync, _, _, _, Oss>( payment_flows::payments_operation_core::<api::PSync, _, _, _, Oss>(
state, state,
merchant_account.clone(), merchant_account.clone(),
@ -93,15 +96,72 @@ impl ProcessTrackerWorkflow<AppState> for PaymentsSyncWorkflow {
let connector = payment_data let connector = payment_data
.payment_attempt .payment_attempt
.connector .connector
.clone()
.ok_or(sch_errors::ProcessTrackerError::MissingRequiredField)?; .ok_or(sch_errors::ProcessTrackerError::MissingRequiredField)?;
retry_sync_task( let is_last_retry = retry_sync_task(
db, db,
connector, connector,
payment_data.payment_attempt.merchant_id, payment_data.payment_attempt.merchant_id.clone(),
process, process,
) )
.await? .await?;
// If the payment status is still processing and there is no connector transaction_id
// then change the payment status to failed if all retries exceeded
if is_last_retry
&& payment_data.payment_attempt.status == enums::AttemptStatus::Pending
&& payment_data
.payment_attempt
.connector_transaction_id
.as_ref()
.is_none()
{
let payment_intent_update = data_models::payments::payment_intent::PaymentIntentUpdate::PGStatusUpdate { status: api_models::enums::IntentStatus::Failed };
let payment_attempt_update =
data_models::payments::payment_attempt::PaymentAttemptUpdate::ErrorUpdate {
connector: None,
status: api_models::enums::AttemptStatus::AuthenticationFailed,
error_code: None,
error_message: None,
error_reason: Some(Some(
consts::REQUEST_TIMEOUT_ERROR_MESSAGE_FROM_PSYNC.to_string(),
)),
amount_capturable: Some(0),
};
payment_data.payment_attempt = db
.update_payment_attempt_with_attempt_id(
payment_data.payment_attempt,
payment_attempt_update,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
payment_data.payment_intent = db
.update_payment_intent(
payment_data.payment_intent,
payment_intent_update,
merchant_account.storage_scheme,
)
.await
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
// Trigger the outgoing webhook to notify the merchant about failed payment
let operation = operations::PaymentStatus;
utils::trigger_payments_webhook::<_, api_models::payments::PaymentsRequest, _>(
merchant_account,
payment_data,
None,
customer,
state,
operation,
)
.await
.map_err(|error| logger::warn!(payments_outgoing_webhook_error=?error))
.ok();
}
} }
}; };
Ok(()) Ok(())
@ -117,6 +177,26 @@ impl ProcessTrackerWorkflow<AppState> for PaymentsSyncWorkflow {
} }
} }
/// Get the next schedule time
///
/// The schedule time can be configured in configs by this key `pt_mapping_trustpay`
/// ```json
/// {
/// "default_mapping": {
/// "start_after": 60,
/// "frequency": [300],
/// "count": [5]
/// },
/// "max_retries_count": 5
/// }
/// ```
///
/// This config represents
///
/// `start_after`: The first psync should happen after 60 seconds
///
/// `frequency` and `count`: The next 5 retries should have an interval of 300 seconds between them
///
pub async fn get_sync_process_schedule_time( pub async fn get_sync_process_schedule_time(
db: &dyn StorageInterface, db: &dyn StorageInterface,
connector: &str, connector: &str,
@ -142,25 +222,32 @@ pub async fn get_sync_process_schedule_time(
process_data::ConnectorPTMapping::default() process_data::ConnectorPTMapping::default()
} }
}; };
let time_delta = utils::get_schedule_time(mapping, merchant_id, retry_count + 1); let time_delta = scheduler_utils::get_schedule_time(mapping, merchant_id, retry_count + 1);
Ok(utils::get_time_from_delta(time_delta)) Ok(scheduler_utils::get_time_from_delta(time_delta))
} }
/// Schedule the task for retry
///
/// Returns bool which indicates whether this was the last retry or not
pub async fn retry_sync_task( pub async fn retry_sync_task(
db: &dyn StorageInterface, db: &dyn StorageInterface,
connector: String, connector: String,
merchant_id: String, merchant_id: String,
pt: storage::ProcessTracker, pt: storage::ProcessTracker,
) -> Result<(), sch_errors::ProcessTrackerError> { ) -> Result<bool, sch_errors::ProcessTrackerError> {
let schedule_time = let schedule_time =
get_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?; get_sync_process_schedule_time(db, &connector, &merchant_id, pt.retry_count).await?;
match schedule_time { match schedule_time {
Some(s_time) => pt.retry(db.as_scheduler(), s_time).await, Some(s_time) => {
pt.retry(db.as_scheduler(), s_time).await?;
Ok(false)
}
None => { None => {
pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string()) pt.finish_with_status(db.as_scheduler(), "RETRIES_EXCEEDED".to_string())
.await .await?;
Ok(true)
} }
} }
} }
@ -173,9 +260,11 @@ mod tests {
#[test] #[test]
fn test_get_default_schedule_time() { fn test_get_default_schedule_time() {
let schedule_time_delta = let schedule_time_delta =
utils::get_schedule_time(process_data::ConnectorPTMapping::default(), "-", 0).unwrap(); scheduler_utils::get_schedule_time(process_data::ConnectorPTMapping::default(), "-", 0)
.unwrap();
let first_retry_time_delta = let first_retry_time_delta =
utils::get_schedule_time(process_data::ConnectorPTMapping::default(), "-", 1).unwrap(); scheduler_utils::get_schedule_time(process_data::ConnectorPTMapping::default(), "-", 1)
.unwrap();
let cpt_default = process_data::ConnectorPTMapping::default().default_mapping; let cpt_default = process_data::ConnectorPTMapping::default().default_mapping;
assert_eq!( assert_eq!(
vec![schedule_time_delta, first_retry_time_delta], vec![schedule_time_delta, first_retry_time_delta],

View File

@ -298,6 +298,7 @@ pub fn get_schedule_time(
None => mapping.default_mapping, None => mapping.default_mapping,
}; };
// For first try, get the `start_after` time
if retry_count == 0 { if retry_count == 0 {
Some(mapping.start_after) Some(mapping.start_after)
} else { } else {
@ -328,6 +329,7 @@ pub fn get_pm_schedule_time(
} }
} }
/// Get the delay based on the retry count
fn get_delay<'a>( fn get_delay<'a>(
retry_count: i32, retry_count: i32,
mut array: impl Iterator<Item = (&'a i32, &'a i32)>, mut array: impl Iterator<Item = (&'a i32, &'a i32)>,