feat(core): [Retry] MIT Retries (#8628)

This commit is contained in:
Sagnik Mitra
2025-09-10 17:28:36 +05:30
committed by GitHub
parent 5ab8a27c10
commit c0e31d38ff
6 changed files with 341 additions and 124 deletions

View File

@ -14,6 +14,8 @@ use common_utils::{
use error_stack::ResultExt; use error_stack::ResultExt;
use time::PrimitiveDateTime; use time::PrimitiveDateTime;
use crate::router_data::RecurringMandatePaymentData;
#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)] #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub struct MandateDetails { pub struct MandateDetails {
@ -174,6 +176,20 @@ pub struct PaymentsMandateReferenceRecord {
pub connector_mandate_request_reference_id: Option<String>, pub connector_mandate_request_reference_id: Option<String>,
} }
#[cfg(feature = "v1")]
impl From<&PaymentsMandateReferenceRecord> for RecurringMandatePaymentData {
fn from(mandate_reference_record: &PaymentsMandateReferenceRecord) -> Self {
Self {
payment_method_type: mandate_reference_record.payment_method_type,
original_payment_authorized_amount: mandate_reference_record
.original_payment_authorized_amount,
original_payment_authorized_currency: mandate_reference_record
.original_payment_authorized_currency,
mandate_metadata: mandate_reference_record.mandate_metadata.clone(),
}
}
}
#[cfg(feature = "v2")] #[cfg(feature = "v2")]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct ConnectorTokenReferenceRecord { pub struct ConnectorTokenReferenceRecord {

View File

@ -693,7 +693,7 @@ fn process_connector_for_networks(
let matching_networks = find_matching_networks( let matching_networks = find_matching_networks(
&merchant_debit_networks, &merchant_debit_networks,
fee_sorted_debit_networks, fee_sorted_debit_networks,
&connector_data.connector_data, connector_data,
debit_routing_config, debit_routing_config,
has_us_local_network, has_us_local_network,
); );
@ -715,13 +715,13 @@ fn find_merchant_connector_account(
fn find_matching_networks( fn find_matching_networks(
merchant_debit_networks: &HashSet<common_enums::CardNetwork>, merchant_debit_networks: &HashSet<common_enums::CardNetwork>,
fee_sorted_debit_networks: &[common_enums::CardNetwork], fee_sorted_debit_networks: &[common_enums::CardNetwork],
connector_data: &api::ConnectorData, connector_routing_data: &api::ConnectorRoutingData,
debit_routing_config: &settings::DebitRoutingConfig, debit_routing_config: &settings::DebitRoutingConfig,
has_us_local_network: &mut bool, has_us_local_network: &mut bool,
) -> Vec<api::ConnectorRoutingData> { ) -> Vec<api::ConnectorRoutingData> {
let is_routing_enabled = debit_routing_config let is_routing_enabled = debit_routing_config
.supported_connectors .supported_connectors
.contains(&connector_data.connector_name); .contains(&connector_routing_data.connector_data.connector_name.clone());
fee_sorted_debit_networks fee_sorted_debit_networks
.iter() .iter()
@ -733,8 +733,9 @@ fn find_matching_networks(
} }
api::ConnectorRoutingData { api::ConnectorRoutingData {
connector_data: connector_data.clone(), connector_data: connector_routing_data.connector_data.clone(),
network: Some(network.clone()), network: Some(network.clone()),
action_type: connector_routing_data.action_type.clone(),
} }
}) })
.collect() .collect()

View File

@ -8981,88 +8981,64 @@ where
.get_required_value("payment_method_info")? .get_required_value("payment_method_info")?
.clone(); .clone();
//fetch connectors that support ntid flow let retryable_connectors =
let ntid_supported_connectors = &state join_all(connectors.into_iter().map(|connector_routing_data| {
.conf let payment_method = payment_method_info.clone();
.network_transaction_id_supported_connectors async move {
.connector_list; let action_types = get_all_action_types(
//filered connectors list with ntid_supported_connectors state,
let filtered_ntid_supported_connectors = is_connector_agnostic_mit_enabled,
filter_ntid_supported_connectors(connectors.clone(), ntid_supported_connectors); is_network_tokenization_enabled,
&payment_method.clone(),
connector_routing_data.connector_data.clone(),
)
.await;
//fetch connectors that support network tokenization flow action_types
let network_tokenization_supported_connectors = &state .into_iter()
.conf .map(|action_type| api::ConnectorRoutingData {
.network_tokenization_supported_connectors connector_data: connector_routing_data.connector_data.clone(),
.connector_list; action_type: Some(action_type),
//filered connectors list with ntid_supported_connectors and network_tokenization_supported_connectors network: connector_routing_data.network.clone(),
let filtered_nt_supported_connectors = filter_network_tokenization_supported_connectors( })
filtered_ntid_supported_connectors, .collect::<Vec<_>>()
network_tokenization_supported_connectors, }
}))
.await
.into_iter()
.flatten()
.collect::<Vec<_>>();
let chosen_connector_routing_data = retryable_connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("no eligible connector found for token-based MIT payment")?;
let mandate_reference_id = get_mandate_reference_id(
chosen_connector_routing_data.action_type.clone(),
chosen_connector_routing_data.clone(),
payment_data,
&payment_method_info,
)?;
routing_data.routed_through = Some(
chosen_connector_routing_data
.connector_data
.connector_name
.to_string(),
); );
let action_type = decide_action_type( routing_data.merchant_connector_id.clone_from(
state, &chosen_connector_routing_data
is_connector_agnostic_mit_enabled, .connector_data
is_network_tokenization_enabled, .merchant_connector_id,
&payment_method_info, );
filtered_nt_supported_connectors.clone(),
)
.await;
match action_type { payment_data.set_mandate_id(payments_api::MandateIds {
Some(ActionType::NetworkTokenWithNetworkTransactionId(nt_data)) => { mandate_id: None,
logger::info!( mandate_reference_id,
"using network_tokenization with network_transaction_id for MIT flow" });
); Ok(ConnectorCallType::Retryable(retryable_connectors))
let mandate_reference_id =
Some(payments_api::MandateReferenceId::NetworkTokenWithNTI(
payments_api::NetworkTokenWithNTIRef {
network_transaction_id: nt_data.network_transaction_id.to_string(),
token_exp_month: nt_data.token_exp_month,
token_exp_year: nt_data.token_exp_year,
},
));
let chosen_connector_data = filtered_nt_supported_connectors
.first()
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable(
"no eligible connector found for token-based MIT payment",
)?;
routing_data.routed_through = Some(
chosen_connector_data
.connector_data
.connector_name
.to_string(),
);
routing_data
.merchant_connector_id
.clone_from(&chosen_connector_data.connector_data.merchant_connector_id);
payment_data.set_mandate_id(payments_api::MandateIds {
mandate_id: None,
mandate_reference_id,
});
Ok(ConnectorCallType::PreDetermined(
chosen_connector_data.clone(),
))
}
None => {
decide_connector_for_normal_or_recurring_payment(
state,
payment_data,
routing_data,
connectors,
is_connector_agnostic_mit_enabled,
&payment_method_info,
)
.await
}
}
} }
( (
None, None,
@ -9112,6 +9088,78 @@ where
} }
} }
#[cfg(feature = "v1")]
pub fn get_mandate_reference_id<F: Clone, D>(
action_type: Option<ActionType>,
connector_routing_data: api::ConnectorRoutingData,
payment_data: &mut D,
payment_method_info: &domain::PaymentMethod,
) -> RouterResult<Option<api_models::payments::MandateReferenceId>>
where
D: OperationSessionGetters<F> + OperationSessionSetters<F> + Send + Sync + Clone,
{
let mandate_reference_id = match action_type {
Some(ActionType::NetworkTokenWithNetworkTransactionId(network_token_data)) => {
logger::info!("using network token with network_transaction_id for MIT flow");
Some(payments_api::MandateReferenceId::NetworkTokenWithNTI(
network_token_data.into(),
))
}
Some(ActionType::CardWithNetworkTransactionId(network_transaction_id)) => {
logger::info!("using card with network_transaction_id for MIT flow");
Some(payments_api::MandateReferenceId::NetworkMandateId(
network_transaction_id,
))
}
Some(ActionType::ConnectorMandate(connector_mandate_details)) => {
logger::info!("using connector_mandate_id for MIT flow");
let merchant_connector_id = connector_routing_data
.connector_data
.merchant_connector_id
.as_ref()
.ok_or_else(|| {
report!(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("No eligible connector found for token-based MIT flow: no connector mandate details")
})?;
let mandate_reference_record = connector_mandate_details
.get(merchant_connector_id)
.ok_or_else(|| {
report!(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
.attach_printable("No mandate record found for merchant connector ID")
})?;
if let Some(mandate_currency) =
mandate_reference_record.original_payment_authorized_currency
{
if mandate_currency != payment_data.get_currency() {
return Err(report!(errors::ApiErrorResponse::MandateValidationFailed {
reason: "Cross currency mandates not supported".into(),
}));
}
}
payment_data.set_recurring_mandate_payment_data(mandate_reference_record.into());
Some(payments_api::MandateReferenceId::ConnectorMandateId(
api_models::payments::ConnectorMandateReferenceId::new(
Some(mandate_reference_record.connector_mandate_id.clone()),
Some(payment_method_info.get_id().clone()),
None,
mandate_reference_record.mandate_metadata.clone(),
mandate_reference_record
.connector_mandate_request_reference_id
.clone(),
),
))
}
None => None,
};
Ok(mandate_reference_id)
}
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub async fn decide_connector_for_normal_or_recurring_payment<F: Clone, D>( pub async fn decide_connector_for_normal_or_recurring_payment<F: Clone, D>(
@ -9178,16 +9226,8 @@ where
) )
)); ));
payment_data.set_recurring_mandate_payment_data( payment_data.set_recurring_mandate_payment_data(
hyperswitch_domain_models::router_data::RecurringMandatePaymentData { mandate_reference_record.into(),
payment_method_type: mandate_reference_record );
.payment_method_type,
original_payment_authorized_amount: mandate_reference_record
.original_payment_authorized_amount,
original_payment_authorized_currency: mandate_reference_record
.original_payment_authorized_currency,
mandate_metadata: mandate_reference_record
.mandate_metadata.clone()
});
connector_choice = Some((connector_data, mandate_reference_id.clone())); connector_choice = Some((connector_data, mandate_reference_id.clone()));
break; break;
} }
@ -9260,9 +9300,23 @@ pub struct NTWithNTIRef {
pub token_exp_year: Option<Secret<String>>, pub token_exp_year: Option<Secret<String>>,
} }
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone, Eq, PartialEq)] impl From<NTWithNTIRef> for payments_api::NetworkTokenWithNTIRef {
fn from(network_token_data: NTWithNTIRef) -> Self {
Self {
network_transaction_id: network_token_data.network_transaction_id,
token_exp_month: network_token_data.token_exp_month,
token_exp_year: network_token_data.token_exp_year,
}
}
}
// This represents the recurring details of a connector which will be used for retries
#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)]
pub enum ActionType { pub enum ActionType {
NetworkTokenWithNetworkTransactionId(NTWithNTIRef), NetworkTokenWithNetworkTransactionId(NTWithNTIRef),
CardWithNetworkTransactionId(String), // Network Transaction Id
#[cfg(feature = "v1")]
ConnectorMandate(hyperswitch_domain_models::mandates::PaymentsMandateReference),
} }
pub fn filter_network_tokenization_supported_connectors( pub fn filter_network_tokenization_supported_connectors(
@ -9278,40 +9332,154 @@ pub fn filter_network_tokenization_supported_connectors(
} }
#[cfg(feature = "v1")] #[cfg(feature = "v1")]
pub async fn decide_action_type( #[derive(Default)]
pub struct ActionTypesBuilder {
action_types: Vec<ActionType>,
}
#[cfg(feature = "v1")]
impl ActionTypesBuilder {
pub fn new() -> Self {
Self {
action_types: Vec::new(),
}
}
pub fn with_mandate_flow(
mut self,
is_mandate_flow: bool,
connector_mandate_details: Option<
hyperswitch_domain_models::mandates::PaymentsMandateReference,
>,
) -> Self {
if is_mandate_flow {
self.action_types.extend(
connector_mandate_details
.map(|details| ActionType::ConnectorMandate(details.to_owned())),
);
}
self
}
pub async fn with_network_tokenization(
mut self,
state: &SessionState,
is_network_token_with_ntid_flow: IsNtWithNtiFlow,
is_nt_with_ntid_supported_connector: bool,
payment_method_info: &domain::PaymentMethod,
) -> Self {
match is_network_token_with_ntid_flow {
IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id)
if is_nt_with_ntid_supported_connector =>
{
self.action_types.extend(
network_tokenization::do_status_check_for_network_token(
state,
payment_method_info,
)
.await
.inspect_err(|e| {
logger::error!("Status check for network token failed: {:?}", e)
})
.ok()
.map(|(token_exp_month, token_exp_year)| {
ActionType::NetworkTokenWithNetworkTransactionId(NTWithNTIRef {
token_exp_month,
token_exp_year,
network_transaction_id,
})
}),
);
}
_ => (),
}
self
}
pub fn with_card_network_transaction_id(
mut self,
is_card_with_ntid_flow: bool,
payment_method_info: &domain::PaymentMethod,
) -> Self {
if is_card_with_ntid_flow {
self.action_types.extend(
payment_method_info
.network_transaction_id
.as_ref()
.map(|ntid| ActionType::CardWithNetworkTransactionId(ntid.clone())),
);
}
self
}
pub fn build(self) -> Vec<ActionType> {
self.action_types
}
}
#[cfg(feature = "v1")]
pub async fn get_all_action_types(
state: &SessionState, state: &SessionState,
is_connector_agnostic_mit_enabled: Option<bool>, is_connector_agnostic_mit_enabled: Option<bool>,
is_network_tokenization_enabled: bool, is_network_tokenization_enabled: bool,
payment_method_info: &domain::PaymentMethod, payment_method_info: &domain::PaymentMethod,
filtered_nt_supported_connectors: Vec<api::ConnectorRoutingData>, //network tokenization supported connectors connector: api::ConnectorData,
) -> Option<ActionType> { ) -> Vec<ActionType> {
match ( let merchant_connector_id = connector.merchant_connector_id.as_ref();
is_network_token_with_network_transaction_id_flow(
is_connector_agnostic_mit_enabled, //fetch connectors that support ntid flow
is_network_tokenization_enabled, let ntid_supported_connectors = &state
.conf
.network_transaction_id_supported_connectors
.connector_list;
//fetch connectors that support network tokenization flow
let network_tokenization_supported_connectors = &state
.conf
.network_tokenization_supported_connectors
.connector_list;
let is_network_token_with_ntid_flow = is_network_token_with_network_transaction_id_flow(
is_connector_agnostic_mit_enabled,
is_network_tokenization_enabled,
payment_method_info,
);
let is_card_with_ntid_flow = is_network_transaction_id_flow(
state,
is_connector_agnostic_mit_enabled,
connector.connector_name,
payment_method_info,
);
let payments_mandate_reference = payment_method_info
.get_common_mandate_reference()
.map_err(|err| {
logger::warn!("Error getting connector mandate details: {:?}", err);
err
})
.ok()
.and_then(|details| details.payments);
let is_mandate_flow = payments_mandate_reference
.clone()
.zip(merchant_connector_id)
.map(|(details, merchant_connector_id)| details.contains_key(merchant_connector_id))
.unwrap_or(false);
let is_nt_with_ntid_supported_connector = ntid_supported_connectors
.contains(&connector.connector_name)
&& network_tokenization_supported_connectors.contains(&connector.connector_name);
ActionTypesBuilder::new()
.with_mandate_flow(is_mandate_flow, payments_mandate_reference)
.with_network_tokenization(
state,
is_network_token_with_ntid_flow,
is_nt_with_ntid_supported_connector,
payment_method_info, payment_method_info,
), )
!filtered_nt_supported_connectors.is_empty(), .await
) { .with_card_network_transaction_id(is_card_with_ntid_flow, payment_method_info)
(IsNtWithNtiFlow::NtWithNtiSupported(network_transaction_id), true) => { .build()
if let Ok((token_exp_month, token_exp_year)) =
network_tokenization::do_status_check_for_network_token(state, payment_method_info)
.await
{
Some(ActionType::NetworkTokenWithNetworkTransactionId(
NTWithNTIRef {
token_exp_month,
token_exp_year,
network_transaction_id,
},
))
} else {
None
}
}
(IsNtWithNtiFlow::NtWithNtiSupported(_), false)
| (IsNtWithNtiFlow::NTWithNTINotSupported, _) => None,
}
} }
pub fn is_network_transaction_id_flow( pub fn is_network_transaction_id_flow(

View File

@ -3,6 +3,7 @@ use std::vec::IntoIter;
use common_utils::{ext_traits::Encode, types::MinorUnit}; use common_utils::{ext_traits::Encode, types::MinorUnit};
use diesel_models::enums as storage_enums; use diesel_models::enums as storage_enums;
use error_stack::ResultExt; use error_stack::ResultExt;
use hyperswitch_domain_models::ext_traits::OptionExt;
use router_env::{ use router_env::{
logger, logger,
tracing::{self, instrument}, tracing::{self, instrument},
@ -159,7 +160,31 @@ where
&& clear_pan_possible && clear_pan_possible
&& business_profile.is_clear_pan_retries_enabled; && business_profile.is_clear_pan_retries_enabled;
let (connector, routing_decision) = if should_retry_with_pan { // Currently we are taking off_session as a source of truth to identify MIT payments.
let is_mit_payment = payment_data
.get_payment_intent()
.off_session
.unwrap_or(false);
let (connector, routing_decision) = if is_mit_payment {
let connector_routing_data =
super::get_connector_data(&mut connector_routing_data)?;
let payment_method_info = payment_data
.get_payment_method_info()
.get_required_value("payment_method_info")?
.clone();
let mandate_reference_id = payments::get_mandate_reference_id(
connector_routing_data.action_type.clone(),
connector_routing_data.clone(),
payment_data,
&payment_method_info,
)?;
payment_data.set_mandate_id(api_models::payments::MandateIds {
mandate_id: None,
mandate_reference_id, //mandate_ref_id
});
(connector_routing_data.connector_data, None)
} else if should_retry_with_pan {
// If should_retry_with_pan is true, it indicates that we are retrying with PAN using the same connector. // If should_retry_with_pan is true, it indicates that we are retrying with PAN using the same connector.
(original_connector_data.clone(), None) (original_connector_data.clone(), None)
} else { } else {

View File

@ -107,6 +107,7 @@ impl From<ConnectorData> for ConnectorRoutingData {
Self { Self {
connector_data, connector_data,
network: None, network: None,
action_type: None,
} }
} }
} }

View File

@ -16,7 +16,11 @@ pub use super::fraud_check_v2::{
FraudCheckTransactionV2, FraudCheckV2, FraudCheckTransactionV2, FraudCheckV2,
}; };
use super::{ConnectorData, SessionConnectorDatas}; use super::{ConnectorData, SessionConnectorDatas};
use crate::{connector, core::errors, services::connector_integration_interface::ConnectorEnum}; use crate::{
connector,
core::{errors, payments::ActionType},
services::connector_integration_interface::ConnectorEnum,
};
#[derive(Clone)] #[derive(Clone)]
pub struct FraudCheckConnectorData { pub struct FraudCheckConnectorData {
@ -33,6 +37,8 @@ pub enum ConnectorCallType {
pub struct ConnectorRoutingData { pub struct ConnectorRoutingData {
pub connector_data: ConnectorData, pub connector_data: ConnectorData,
pub network: Option<common_enums::CardNetwork>, pub network: Option<common_enums::CardNetwork>,
// action_type is used for mandates currently
pub action_type: Option<ActionType>,
} }
impl FraudCheckConnectorData { impl FraudCheckConnectorData {