mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-03 13:30:39 +08:00
feat(payouts): extend routing capabilities to payout operation (#3531)
Co-authored-by: Kashif <mohammed.kashif@juspay.in>
This commit is contained in:
@ -1,11 +1,13 @@
|
||||
use api_models::enums::PayoutConnectors;
|
||||
use common_utils::{
|
||||
errors::CustomResult,
|
||||
ext_traits::{AsyncExt, StringExt, ValueExt},
|
||||
ext_traits::{AsyncExt, StringExt},
|
||||
};
|
||||
use diesel_models::encryption::Encryption;
|
||||
use error_stack::{IntoReport, ResultExt};
|
||||
use masking::{ExposeInterface, PeekInterface, Secret};
|
||||
|
||||
use super::PayoutData;
|
||||
use crate::{
|
||||
core::{
|
||||
errors::{self, RouterResult},
|
||||
@ -14,7 +16,11 @@ use crate::{
|
||||
transformers::{StoreCardReq, StoreGenericReq, StoreLockerReq},
|
||||
vault,
|
||||
},
|
||||
payments::{customers::get_connector_customer_details_if_present, CustomerDetails},
|
||||
payments::{
|
||||
customers::get_connector_customer_details_if_present, route_connector_v1, routing,
|
||||
CustomerDetails,
|
||||
},
|
||||
routing::TransactionData,
|
||||
utils as core_utils,
|
||||
},
|
||||
db::StorageInterface,
|
||||
@ -433,118 +439,202 @@ pub async fn get_or_create_customer_details(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decide_payout_connector(
|
||||
pub async fn decide_payout_connector(
|
||||
state: &AppState,
|
||||
merchant_account: &domain::MerchantAccount,
|
||||
request_straight_through: Option<api::PayoutStraightThroughAlgorithm>,
|
||||
routing_data: &mut storage::PayoutRoutingData,
|
||||
) -> RouterResult<api::PayoutConnectorCallType> {
|
||||
if let Some(ref connector_name) = routing_data.routed_through {
|
||||
let connector_data = api::PayoutConnectorData::get_connector_by_name(
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
request_straight_through: Option<api::routing::StraightThroughAlgorithm>,
|
||||
routing_data: &mut storage::RoutingData,
|
||||
payout_data: &mut PayoutData,
|
||||
eligible_connectors: Option<Vec<api_models::enums::RoutableConnectors>>,
|
||||
) -> RouterResult<api::ConnectorCallType> {
|
||||
// 1. For existing attempts, use stored connector
|
||||
let payout_attempt = &payout_data.payout_attempt;
|
||||
if let Some(connector_name) = payout_attempt.connector.clone() {
|
||||
// Connector was already decided previously, use the same connector
|
||||
let connector_data = api::ConnectorData::get_payout_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
connector_name,
|
||||
&connector_name,
|
||||
api::GetToken::Connector,
|
||||
payout_attempt.merchant_connector_id.clone(),
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid connector name received in 'routed_through'")?;
|
||||
|
||||
return Ok(api::PayoutConnectorCallType::Single(connector_data));
|
||||
routing_data.routed_through = Some(connector_name.clone());
|
||||
return Ok(api::ConnectorCallType::PreDetermined(connector_data));
|
||||
}
|
||||
|
||||
// 2. Check routing algorithm passed in the request
|
||||
if let Some(routing_algorithm) = request_straight_through {
|
||||
let connector_name = match &routing_algorithm {
|
||||
api::PayoutStraightThroughAlgorithm::Single(conn) => conn.to_string(),
|
||||
};
|
||||
let (mut connectors, check_eligibility) =
|
||||
routing::perform_straight_through_routing(&routing_algorithm, None)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed execution of straight through routing")?;
|
||||
|
||||
let connector_data = api::PayoutConnectorData::get_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&connector_name,
|
||||
api::GetToken::Connector,
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid connector name received in routing algorithm")?;
|
||||
if check_eligibility {
|
||||
connectors = routing::perform_eligibility_analysis_with_fallback(
|
||||
state,
|
||||
key_store,
|
||||
merchant_account.modified_at.assume_utc().unix_timestamp(),
|
||||
connectors,
|
||||
&TransactionData::<()>::Payout(payout_data),
|
||||
eligible_connectors,
|
||||
#[cfg(feature = "business_profile_routing")]
|
||||
Some(payout_attempt.profile_id.clone()),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("failed eligibility analysis and fallback")?;
|
||||
}
|
||||
|
||||
routing_data.routed_through = Some(connector_name);
|
||||
routing_data.algorithm = Some(routing_algorithm);
|
||||
return Ok(api::PayoutConnectorCallType::Single(connector_data));
|
||||
let first_connector_choice = connectors
|
||||
.first()
|
||||
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
|
||||
.into_report()
|
||||
.attach_printable("Empty connector list returned")?
|
||||
.clone();
|
||||
|
||||
let connector_data = connectors
|
||||
.into_iter()
|
||||
.map(|conn| {
|
||||
api::ConnectorData::get_payout_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&conn.connector.to_string(),
|
||||
api::GetToken::Connector,
|
||||
#[cfg(feature = "connector_choice_mca_id")]
|
||||
payout_attempt.merchant_connector_id.clone(),
|
||||
#[cfg(not(feature = "connector_choice_mca_id"))]
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect::<CustomResult<Vec<_>, _>>()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid connector name received")?;
|
||||
|
||||
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
|
||||
#[cfg(feature = "connector_choice_mca_id")]
|
||||
{
|
||||
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
|
||||
}
|
||||
#[cfg(not(feature = "connector_choice_mca_id"))]
|
||||
{
|
||||
routing_data.business_sub_label = first_connector_choice.sub_label.clone();
|
||||
}
|
||||
routing_data.routing_info.algorithm = Some(routing_algorithm);
|
||||
return Ok(api::ConnectorCallType::Retryable(connector_data));
|
||||
}
|
||||
|
||||
// 3. Check algorithm passed in routing data
|
||||
if let Some(ref routing_algorithm) = routing_data.algorithm {
|
||||
let connector_name = match routing_algorithm {
|
||||
api::PayoutStraightThroughAlgorithm::Single(conn) => conn.to_string(),
|
||||
};
|
||||
let (mut connectors, check_eligibility) =
|
||||
routing::perform_straight_through_routing(routing_algorithm, None)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed execution of straight through routing")?;
|
||||
|
||||
let connector_data = api::PayoutConnectorData::get_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&connector_name,
|
||||
api::GetToken::Connector,
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid connector name received in routing algorithm")?;
|
||||
if check_eligibility {
|
||||
connectors = routing::perform_eligibility_analysis_with_fallback(
|
||||
state,
|
||||
key_store,
|
||||
merchant_account.modified_at.assume_utc().unix_timestamp(),
|
||||
connectors,
|
||||
&TransactionData::<()>::Payout(payout_data),
|
||||
eligible_connectors,
|
||||
#[cfg(feature = "business_profile_routing")]
|
||||
Some(payout_attempt.profile_id.clone()),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("failed eligibility analysis and fallback")?;
|
||||
}
|
||||
|
||||
routing_data.routed_through = Some(connector_name);
|
||||
return Ok(api::PayoutConnectorCallType::Single(connector_data));
|
||||
let first_connector_choice = connectors
|
||||
.first()
|
||||
.ok_or(errors::ApiErrorResponse::IncorrectPaymentMethodConfiguration)
|
||||
.into_report()
|
||||
.attach_printable("Empty connector list returned")?
|
||||
.clone();
|
||||
|
||||
connectors.remove(0);
|
||||
|
||||
let connector_data = connectors
|
||||
.into_iter()
|
||||
.map(|conn| {
|
||||
api::ConnectorData::get_payout_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&conn.connector.to_string(),
|
||||
api::GetToken::Connector,
|
||||
#[cfg(feature = "connector_choice_mca_id")]
|
||||
payout_attempt.merchant_connector_id.clone(),
|
||||
#[cfg(not(feature = "connector_choice_mca_id"))]
|
||||
None,
|
||||
)
|
||||
})
|
||||
.collect::<CustomResult<Vec<_>, _>>()
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Invalid connector name received")?;
|
||||
|
||||
routing_data.routed_through = Some(first_connector_choice.connector.to_string());
|
||||
#[cfg(feature = "connector_choice_mca_id")]
|
||||
{
|
||||
routing_data.merchant_connector_id = first_connector_choice.merchant_connector_id;
|
||||
}
|
||||
#[cfg(not(feature = "connector_choice_mca_id"))]
|
||||
{
|
||||
routing_data.business_sub_label = first_connector_choice.sub_label.clone();
|
||||
}
|
||||
return Ok(api::ConnectorCallType::Retryable(connector_data));
|
||||
}
|
||||
|
||||
let routing_algorithm = merchant_account
|
||||
.payout_routing_algorithm
|
||||
.clone()
|
||||
.get_required_value("PayoutRoutingAlgorithm")
|
||||
.change_context(errors::ApiErrorResponse::PreconditionFailed {
|
||||
message: "no routing algorithm has been configured".to_string(),
|
||||
})?
|
||||
.parse_value::<api::PayoutRoutingAlgorithm>("PayoutRoutingAlgorithm")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError) // Deserialization failed
|
||||
.attach_printable("Unable to deserialize merchant routing algorithm")?;
|
||||
|
||||
let connector_name = match routing_algorithm {
|
||||
api::PayoutRoutingAlgorithm::Single(conn) => conn.to_string(),
|
||||
};
|
||||
|
||||
let connector_data = api::PayoutConnectorData::get_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&connector_name,
|
||||
api::GetToken::Connector,
|
||||
// 4. Route connector
|
||||
route_connector_v1(
|
||||
state,
|
||||
merchant_account,
|
||||
&payout_data.business_profile,
|
||||
key_store,
|
||||
&TransactionData::<()>::Payout(payout_data),
|
||||
routing_data,
|
||||
eligible_connectors,
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Routing algorithm gave invalid connector")?;
|
||||
|
||||
routing_data.routed_through = Some(connector_name);
|
||||
|
||||
Ok(api::PayoutConnectorCallType::Single(connector_data))
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_default_payout_connector(
|
||||
_state: &AppState,
|
||||
request_connector: Option<serde_json::Value>,
|
||||
) -> CustomResult<api::PayoutConnectorChoice, errors::ApiErrorResponse> {
|
||||
) -> CustomResult<api::ConnectorChoice, errors::ApiErrorResponse> {
|
||||
Ok(request_connector.map_or(
|
||||
api::PayoutConnectorChoice::Decide,
|
||||
api::PayoutConnectorChoice::StraightThrough,
|
||||
api::ConnectorChoice::Decide,
|
||||
api::ConnectorChoice::StraightThrough,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn should_call_payout_connector_create_customer<'a>(
|
||||
state: &AppState,
|
||||
connector: &api::PayoutConnectorData,
|
||||
connector: &api::ConnectorData,
|
||||
customer: &'a Option<domain::Customer>,
|
||||
connector_label: &str,
|
||||
) -> (bool, Option<&'a str>) {
|
||||
// Check if create customer is required for the connector
|
||||
let connector_needs_customer = state
|
||||
.conf
|
||||
.connector_customer
|
||||
.payout_connector_list
|
||||
.contains(&connector.connector_name);
|
||||
match PayoutConnectors::try_from(connector.connector_name) {
|
||||
Ok(connector) => {
|
||||
let connector_needs_customer = state
|
||||
.conf
|
||||
.connector_customer
|
||||
.payout_connector_list
|
||||
.contains(&connector);
|
||||
|
||||
if connector_needs_customer {
|
||||
let connector_customer_details = customer.as_ref().and_then(|customer| {
|
||||
get_connector_customer_details_if_present(customer, connector_label)
|
||||
});
|
||||
let should_call_connector = connector_customer_details.is_none();
|
||||
(should_call_connector, connector_customer_details)
|
||||
} else {
|
||||
(false, None)
|
||||
if connector_needs_customer {
|
||||
let connector_customer_details = customer.as_ref().and_then(|customer| {
|
||||
get_connector_customer_details_if_present(customer, connector_label)
|
||||
});
|
||||
let should_call_connector = connector_customer_details.is_none();
|
||||
(should_call_connector, connector_customer_details)
|
||||
} else {
|
||||
(false, None)
|
||||
}
|
||||
}
|
||||
_ => (false, None),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user