diff --git a/.github/workflows/validate-openapi-spec.yml b/.github/workflows/validate-openapi-spec.yml index 210f8206483..f14e61781dc 100644 --- a/.github/workflows/validate-openapi-spec.yml +++ b/.github/workflows/validate-openapi-spec.yml @@ -80,6 +80,6 @@ jobs: shell: bash run: | if ! git diff --quiet --exit-code -- openapi/openapi_spec.json ; then - echo '::error::The OpenAPI spec file is not up-to-date. Please re-generate the OpenAPI spec file using `cargo run --features openapi -- generate-openapi-spec` and commit it.' + echo '::error::The OpenAPI spec file is not up-to-date. Please re-generate the OpenAPI spec file using `cargo run -p openapi` and commit it.' exit 1 fi diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index b61f3dcb1c6..9f1008f3d49 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -50,10 +50,6 @@ pub struct MerchantAccountCreate { /// The routing algorithm to be used for routing payouts to desired connectors #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false. @@ -136,10 +132,6 @@ pub struct MerchantAccountUpdate { /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false. @@ -228,10 +220,6 @@ pub struct MerchantAccountResponse { /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// A boolean value to indicate if the merchant is a sub-merchant under a master or a parent merchant. By default, its value is false. @@ -322,124 +310,6 @@ pub struct MerchantDetails { /// The merchant's address details pub address: Option, } -#[cfg(feature = "payouts")] -pub mod payout_routing_algorithm { - use std::{fmt, str::FromStr}; - - use serde::{ - de::{self, Visitor}, - Deserializer, - }; - use serde_json::Map; - - use super::PayoutRoutingAlgorithm; - use crate::enums::PayoutConnectors; - struct RoutingAlgorithmVisitor; - struct OptionalRoutingAlgorithmVisitor; - - impl<'de> Visitor<'de> for RoutingAlgorithmVisitor { - type Value = serde_json::Value; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("routing algorithm") - } - - fn visit_map(self, mut map: A) -> Result - where - A: de::MapAccess<'de>, - { - let mut output = Map::new(); - let mut routing_data: String = "".to_string(); - let mut routing_type: String = "".to_string(); - - while let Some(key) = map.next_key()? { - match key { - "type" => { - routing_type = map.next_value()?; - output.insert( - "type".to_string(), - serde_json::Value::String(routing_type.to_owned()), - ); - } - "data" => { - routing_data = map.next_value()?; - output.insert( - "data".to_string(), - serde_json::Value::String(routing_data.to_owned()), - ); - } - f => { - output.insert(f.to_string(), map.next_value()?); - } - } - } - - match routing_type.as_ref() { - "single" => { - let routable_payout_connector = PayoutConnectors::from_str(&routing_data); - let routable_conn = match routable_payout_connector { - Ok(rpc) => Ok(rpc), - Err(_) => Err(de::Error::custom(format!( - "Unknown payout connector {routing_data}" - ))), - }?; - Ok(PayoutRoutingAlgorithm::Single(routable_conn)) - } - u => Err(de::Error::custom(format!("Unknown routing algorithm {u}"))), - }?; - Ok(serde_json::Value::Object(output)) - } - } - - impl<'de> Visitor<'de> for OptionalRoutingAlgorithmVisitor { - type Value = Option; - - fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - formatter.write_str("routing algorithm") - } - - fn visit_some(self, deserializer: D) -> Result - where - D: Deserializer<'de>, - { - deserializer - .deserialize_any(RoutingAlgorithmVisitor) - .map(Some) - } - - fn visit_none(self) -> Result - where - E: de::Error, - { - Ok(None) - } - - fn visit_unit(self) -> Result - where - E: de::Error, - { - Ok(None) - } - } - - #[allow(dead_code)] - pub(crate) fn deserialize<'a, D>(deserializer: D) -> Result - where - D: Deserializer<'a>, - { - deserializer.deserialize_any(RoutingAlgorithmVisitor) - } - - pub(crate) fn deserialize_option<'a, D>( - deserializer: D, - ) -> Result, D::Error> - where - D: Deserializer<'a>, - { - deserializer.deserialize_option(OptionalRoutingAlgorithmVisitor) - } -} - #[derive(Clone, Debug, Deserialize, ToSchema, Serialize, PartialEq)] #[serde(deny_unknown_fields)] pub struct PrimaryBusinessDetails { @@ -971,13 +841,6 @@ pub enum PayoutRoutingAlgorithm { Single(api_enums::PayoutConnectors), } -#[cfg(feature = "payouts")] -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(tag = "type", content = "data", rename_all = "snake_case")] -pub enum PayoutStraightThroughAlgorithm { - Single(api_enums::PayoutConnectors), -} - #[derive(Clone, Debug, Deserialize, ToSchema, Default, Serialize)] #[serde(deny_unknown_fields)] pub struct BusinessProfileCreate { @@ -1023,10 +886,6 @@ pub struct BusinessProfileCreate { /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// Verified applepay domains for a particular profile @@ -1093,10 +952,6 @@ pub struct BusinessProfileResponse { /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// Verified applepay domains for a particular profile @@ -1155,10 +1010,6 @@ pub struct BusinessProfileUpdate { /// The routing algorithm to be used to process the incoming request from merchant to outgoing payment processor or payment method. The default is 'Custom' #[cfg(feature = "payouts")] #[schema(value_type = Option,example = json!({"type": "single", "data": "wise"}))] - #[serde( - default, - deserialize_with = "payout_routing_algorithm::deserialize_option" - )] pub payout_routing_algorithm: Option, /// Verified applepay domains for a particular profile diff --git a/crates/api_models/src/enums.rs b/crates/api_models/src/enums.rs index 2d9801272c2..45851e4c11b 100644 --- a/crates/api_models/src/enums.rs +++ b/crates/api_models/src/enums.rs @@ -181,6 +181,28 @@ impl From for RoutableConnectors { } } +#[cfg(feature = "payouts")] +impl From for Connector { + fn from(value: PayoutConnectors) -> Self { + match value { + PayoutConnectors::Adyen => Self::Adyen, + PayoutConnectors::Wise => Self::Wise, + } + } +} + +#[cfg(feature = "payouts")] +impl TryFrom for PayoutConnectors { + type Error = String; + fn try_from(value: Connector) -> Result { + match value { + Connector::Adyen => Ok(Self::Adyen), + Connector::Wise => Ok(Self::Wise), + _ => Err(format!("Invalid payout connector {}", value)), + } + } +} + #[cfg(feature = "frm")] #[derive( Clone, diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index b0d0cd75fa9..f1d673bf1fd 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -7,7 +7,7 @@ use masking::Secret; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::{admin, enums as api_enums, payments}; +use crate::{enums as api_enums, payments}; #[derive(Debug, Deserialize, Serialize, Clone, ToSchema)] pub enum PayoutRequest { @@ -48,10 +48,6 @@ pub struct PayoutCreateRequest { "type": "single", "data": "adyen" }))] - #[serde( - default, - deserialize_with = "admin::payout_routing_algorithm::deserialize_option" - )] pub routing: Option, /// This allows the merchant to manually select a connector with which the payout can go through diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 2775034c88c..b82e5433e5a 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -12,7 +12,7 @@ pub use euclid::{ use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::enums::{self, RoutableConnectors}; +use crate::enums::{self, RoutableConnectors, TransactionType}; #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(tag = "type", content = "data", rename_all = "snake_case")] @@ -85,6 +85,7 @@ pub struct MerchantRoutingAlgorithm { pub algorithm: RoutingAlgorithm, pub created_at: i64, pub modified_at: i64, + pub algorithm_for: TransactionType, } impl EuclidDirFilter for ConnectorSelection { @@ -538,6 +539,7 @@ pub struct RoutingDictionaryRecord { pub description: String, pub created_at: i64, pub modified_at: i64, + pub algorithm_for: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)] diff --git a/crates/common_enums/Cargo.toml b/crates/common_enums/Cargo.toml index f82d8e7a825..86e89a1bfdd 100644 --- a/crates/common_enums/Cargo.toml +++ b/crates/common_enums/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [features] dummy_connector = [] openapi = [] +payouts = [] [dependencies] diesel = { version = "2.1.0", features = ["postgres"] } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index b8168fc5329..c3976321f0a 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -2168,6 +2168,29 @@ pub enum ConnectorStatus { Active, } +#[derive( + Clone, + Copy, + Debug, + Eq, + PartialEq, + strum::Display, + strum::EnumString, + serde::Deserialize, + serde::Serialize, + ToSchema, + Default, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum TransactionType { + #[default] + Payment, + #[cfg(feature = "payouts")] + Payout, +} + #[derive( Clone, Copy, diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index 3ee7e811873..a616f302cfc 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -18,7 +18,7 @@ pub mod diesel_exports { DbRefundStatus as RefundStatus, DbRefundType as RefundType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRoleScope as RoleScope, DbRoutingAlgorithmKind as RoutingAlgorithmKind, - DbUserStatus as UserStatus, + DbTransactionType as TransactionType, DbUserStatus as UserStatus, }; } pub use common_enums::*; diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 7a2c8306187..014e6597c9c 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -13,7 +13,7 @@ pub struct PayoutAttempt { pub customer_id: String, pub merchant_id: String, pub address_id: String, - pub connector: String, + pub connector: Option, pub connector_payout_id: String, pub payout_token: Option, pub status: storage_enums::PayoutStatus, @@ -28,6 +28,7 @@ pub struct PayoutAttempt { pub last_modified_at: PrimitiveDateTime, pub profile_id: String, pub merchant_connector_id: Option, + pub routing_info: Option, } impl Default for PayoutAttempt { @@ -40,7 +41,7 @@ impl Default for PayoutAttempt { customer_id: String::default(), merchant_id: String::default(), address_id: String::default(), - connector: String::default(), + connector: None, connector_payout_id: String::default(), payout_token: None, status: storage_enums::PayoutStatus::default(), @@ -53,6 +54,7 @@ impl Default for PayoutAttempt { last_modified_at: now, profile_id: String::default(), merchant_connector_id: None, + routing_info: None, } } } @@ -76,7 +78,7 @@ pub struct PayoutAttemptNew { pub customer_id: String, pub merchant_id: String, pub address_id: String, - pub connector: String, + pub connector: Option, pub connector_payout_id: String, pub payout_token: Option, pub status: storage_enums::PayoutStatus, @@ -91,6 +93,7 @@ pub struct PayoutAttemptNew { pub last_modified_at: Option, pub profile_id: Option, pub merchant_connector_id: Option, + pub routing_info: Option, } #[derive(Debug)] @@ -112,6 +115,10 @@ pub enum PayoutAttemptUpdate { business_label: Option, last_modified_at: Option, }, + UpdateRouting { + connector: String, + routing_info: Option, + }, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -126,6 +133,8 @@ pub struct PayoutAttemptUpdateInternal { pub business_country: Option, pub business_label: Option, pub last_modified_at: Option, + pub connector: Option, + pub routing_info: Option, } impl From for PayoutAttemptUpdateInternal { @@ -165,6 +174,14 @@ impl From for PayoutAttemptUpdateInternal { last_modified_at, ..Default::default() }, + PayoutAttemptUpdate::UpdateRouting { + connector, + routing_info, + } => Self { + connector: Some(connector), + routing_info, + ..Default::default() + }, } } } diff --git a/crates/diesel_models/src/query/routing_algorithm.rs b/crates/diesel_models/src/query/routing_algorithm.rs index 533ac7194c4..b2a9687aa40 100644 --- a/crates/diesel_models/src/query/routing_algorithm.rs +++ b/crates/diesel_models/src/query/routing_algorithm.rs @@ -64,6 +64,7 @@ impl RoutingAlgorithm { dsl::kind, dsl::created_at, dsl::modified_at, + dsl::algorithm_for, )) .filter( dsl::algorithm_id @@ -79,6 +80,7 @@ impl RoutingAlgorithm { enums::RoutingAlgorithmKind, PrimitiveDateTime, PrimitiveDateTime, + enums::TransactionType, )>(conn) .await .into_report() @@ -88,7 +90,16 @@ impl RoutingAlgorithm { .ok_or(DatabaseError::NotFound) .into_report() .map( - |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + |( + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + algorithm_for, + )| { RoutingProfileMetadata { profile_id, algorithm_id, @@ -97,6 +108,7 @@ impl RoutingAlgorithm { kind, created_at, modified_at, + algorithm_for, } }, ) @@ -117,6 +129,7 @@ impl RoutingAlgorithm { dsl::kind, dsl::created_at, dsl::modified_at, + dsl::algorithm_for, )) .filter(dsl::profile_id.eq(profile_id.to_owned())) .limit(limit) @@ -128,13 +141,22 @@ impl RoutingAlgorithm { enums::RoutingAlgorithmKind, PrimitiveDateTime, PrimitiveDateTime, + enums::TransactionType, )>(conn) .await .into_report() .change_context(DatabaseError::Others)? .into_iter() .map( - |(algorithm_id, name, description, kind, created_at, modified_at)| { + |( + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + algorithm_for, + )| { RoutingAlgorithmMetadata { algorithm_id, name, @@ -142,6 +164,7 @@ impl RoutingAlgorithm { kind, created_at, modified_at, + algorithm_for, } }, ) @@ -164,6 +187,7 @@ impl RoutingAlgorithm { dsl::kind, dsl::created_at, dsl::modified_at, + dsl::algorithm_for, )) .filter(dsl::merchant_id.eq(merchant_id.to_owned())) .limit(limit) @@ -177,13 +201,23 @@ impl RoutingAlgorithm { enums::RoutingAlgorithmKind, PrimitiveDateTime, PrimitiveDateTime, + enums::TransactionType, )>(conn) .await .into_report() .change_context(DatabaseError::Others)? .into_iter() .map( - |(profile_id, algorithm_id, name, description, kind, created_at, modified_at)| { + |( + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + algorithm_for, + )| { RoutingProfileMetadata { profile_id, algorithm_id, @@ -192,6 +226,71 @@ impl RoutingAlgorithm { kind, created_at, modified_at, + algorithm_for, + } + }, + ) + .collect()) + } + + #[instrument(skip(conn))] + pub async fn list_metadata_by_merchant_id_transaction_type( + conn: &PgPooledConn, + merchant_id: &str, + transaction_type: &enums::TransactionType, + limit: i64, + offset: i64, + ) -> StorageResult> { + Ok(Self::table() + .select(( + dsl::profile_id, + dsl::algorithm_id, + dsl::name, + dsl::description, + dsl::kind, + dsl::created_at, + dsl::modified_at, + dsl::algorithm_for, + )) + .filter(dsl::merchant_id.eq(merchant_id.to_owned())) + .filter(dsl::algorithm_for.eq(transaction_type.to_owned())) + .limit(limit) + .offset(offset) + .order(dsl::modified_at.desc()) + .load_async::<( + String, + String, + String, + Option, + enums::RoutingAlgorithmKind, + PrimitiveDateTime, + PrimitiveDateTime, + enums::TransactionType, + )>(conn) + .await + .into_report() + .change_context(DatabaseError::Others)? + .into_iter() + .map( + |( + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + algorithm_for, + )| { + RoutingProfileMetadata { + profile_id, + algorithm_id, + name, + description, + kind, + created_at, + modified_at, + algorithm_for, } }, ) diff --git a/crates/diesel_models/src/routing_algorithm.rs b/crates/diesel_models/src/routing_algorithm.rs index 09f9baf7edb..11e80f29daf 100644 --- a/crates/diesel_models/src/routing_algorithm.rs +++ b/crates/diesel_models/src/routing_algorithm.rs @@ -15,6 +15,7 @@ pub struct RoutingAlgorithm { pub algorithm_data: serde_json::Value, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, + pub algorithm_for: enums::TransactionType, } pub struct RoutingAlgorithmMetadata { @@ -24,6 +25,7 @@ pub struct RoutingAlgorithmMetadata { pub kind: enums::RoutingAlgorithmKind, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, + pub algorithm_for: enums::TransactionType, } pub struct RoutingProfileMetadata { @@ -34,4 +36,5 @@ pub struct RoutingProfileMetadata { pub kind: enums::RoutingAlgorithmKind, pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, + pub algorithm_for: enums::TransactionType, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 5093f0df7d0..16ecdb7f7bd 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -847,7 +847,7 @@ diesel::table! { #[max_length = 64] address_id -> Varchar, #[max_length = 64] - connector -> Varchar, + connector -> Nullable, #[max_length = 128] connector_payout_id -> Varchar, #[max_length = 64] @@ -866,6 +866,7 @@ diesel::table! { profile_id -> Varchar, #[max_length = 32] merchant_connector_id -> Nullable, + routing_info -> Nullable, } } @@ -1040,6 +1041,7 @@ diesel::table! { algorithm_data -> Jsonb, created_at -> Timestamp, modified_at -> Timestamp, + algorithm_for -> TransactionType, } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index dc38181a683..ef573093be5 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -406,6 +406,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::enums::PayoutEntityType, api_models::enums::PayoutStatus, api_models::enums::PayoutType, + api_models::enums::TransactionType, api_models::payments::FrmMessage, api_models::webhooks::OutgoingWebhook, api_models::webhooks::OutgoingWebhookContent, diff --git a/crates/router/Cargo.toml b/crates/router/Cargo.toml index c8bdb5b63fc..c511947cd1e 100644 --- a/crates/router/Cargo.toml +++ b/crates/router/Cargo.toml @@ -29,7 +29,7 @@ dummy_connector = ["api_models/dummy_connector", "euclid/dummy_connector", "kgra connector_choice_mca_id = ["api_models/connector_choice_mca_id", "euclid/connector_choice_mca_id", "kgraph_utils/connector_choice_mca_id"] external_access_dc = ["dummy_connector"] detailed_errors = ["api_models/detailed_errors", "error-stack/serde"] -payouts = [] +payouts = ["common_enums/payouts"] recon = ["email", "api_models/recon"] retry = [] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index f45ad5ee4e4..87886c2c861 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -947,11 +947,22 @@ pub async fn create_payment_connector( status: connector_status, }; + let transaction_type = match req.connector_type { + #[cfg(feature = "payouts")] + api_enums::ConnectorType::PayoutProcessor => api_enums::TransactionType::Payout, + _ => api_enums::TransactionType::Payment, + }; + let mut default_routing_config = - routing_helpers::get_merchant_default_config(&*state.store, merchant_id).await?; + routing_helpers::get_merchant_default_config(&*state.store, merchant_id, &transaction_type) + .await?; - let mut default_routing_config_for_profile = - routing_helpers::get_merchant_default_config(&*state.clone().store, &profile_id).await?; + let mut default_routing_config_for_profile = routing_helpers::get_merchant_default_config( + &*state.clone().store, + &profile_id, + &transaction_type, + ) + .await?; let mca = state .store @@ -981,6 +992,7 @@ pub async fn create_payment_connector( &*state.store, merchant_id, default_routing_config.clone(), + &transaction_type, ) .await?; } @@ -990,6 +1002,7 @@ pub async fn create_payment_connector( &*state.store, &profile_id.clone(), default_routing_config_for_profile.clone(), + &transaction_type, ) .await?; } diff --git a/crates/router/src/core/payment_methods/cards.rs b/crates/router/src/core/payment_methods/cards.rs index dbd8efcd1cf..238e0ef4b86 100644 --- a/crates/router/src/core/payment_methods/cards.rs +++ b/crates/router/src/core/payment_methods/cards.rs @@ -1321,7 +1321,7 @@ pub async fn list_payment_methods( payment_intent, chosen, }; - let result = routing::perform_session_flow_routing(sfr) + let result = routing::perform_session_flow_routing(sfr, &enums::TransactionType::Payment) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing session flow routing")?; diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 7348a2b8f0c..97fba548b37 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -40,7 +40,9 @@ use self::{ operations::{payment_complete_authorize, BoxedOperation, Operation}, routing::{self as self_routing, SessionFlowRoutingInput}, }; -use super::{errors::StorageErrorExt, payment_methods::surcharge_decision_configs}; +use super::{ + errors::StorageErrorExt, payment_methods::surcharge_decision_configs, routing::TransactionData, +}; #[cfg(feature = "frm")] use crate::core::fraud_check as frm_core; use crate::{ @@ -2660,10 +2662,12 @@ where } if let Some(routing_algorithm) = request_straight_through { - let (mut connectors, check_eligibility) = - routing::perform_straight_through_routing(&routing_algorithm, payment_data) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed execution of straight through routing")?; + let (mut connectors, check_eligibility) = routing::perform_straight_through_routing( + &routing_algorithm, + payment_data.creds_identifier.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; if check_eligibility { connectors = routing::perform_eligibility_analysis_with_fallback( @@ -2671,7 +2675,7 @@ where key_store, merchant_account.modified_at.assume_utc().unix_timestamp(), connectors, - payment_data, + &TransactionData::Payment(payment_data), eligible_connectors, #[cfg(feature = "business_profile_routing")] payment_data.payment_intent.profile_id.clone(), @@ -2719,10 +2723,12 @@ where } if let Some(ref routing_algorithm) = routing_data.routing_info.algorithm { - let (mut connectors, check_eligibility) = - routing::perform_straight_through_routing(routing_algorithm, payment_data) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed execution of straight through routing")?; + let (mut connectors, check_eligibility) = routing::perform_straight_through_routing( + routing_algorithm, + payment_data.creds_identifier.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed execution of straight through routing")?; if check_eligibility { connectors = routing::perform_eligibility_analysis_with_fallback( @@ -2730,7 +2736,7 @@ where key_store, merchant_account.modified_at.assume_utc().unix_timestamp(), connectors, - payment_data, + &TransactionData::Payment(payment_data), eligible_connectors, #[cfg(feature = "business_profile_routing")] payment_data.payment_intent.profile_id.clone(), @@ -2781,7 +2787,7 @@ where merchant_account, business_profile, key_store, - payment_data, + &TransactionData::Payment(payment_data), routing_data, eligible_connectors, ) @@ -2879,7 +2885,7 @@ where chosen, }; - let result = self_routing::perform_session_flow_routing(sfr) + let result = self_routing::perform_session_flow_routing(sfr, &enums::TransactionType::Payment) .await .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("error performing session flow routing")?; @@ -2917,17 +2923,36 @@ pub async fn route_connector_v1( merchant_account: &domain::MerchantAccount, business_profile: &storage::business_profile::BusinessProfile, key_store: &domain::MerchantKeyStore, - payment_data: &mut PaymentData, + transaction_data: &TransactionData<'_, F>, routing_data: &mut storage::RoutingData, eligible_connectors: Option>, ) -> RouterResult where F: Send + Clone, { - let routing_algorithm = if cfg!(feature = "business_profile_routing") { - business_profile.routing_algorithm.clone() - } else { - merchant_account.routing_algorithm.clone() + #[allow(unused_variables)] + let (profile_id, routing_algorithm) = match transaction_data { + TransactionData::Payment(payment_data) => { + if cfg!(feature = "business_profile_routing") { + ( + payment_data.payment_intent.profile_id.clone(), + business_profile.routing_algorithm.clone(), + ) + } else { + (None, merchant_account.routing_algorithm.clone()) + } + } + #[cfg(feature = "payouts")] + TransactionData::Payout(payout_data) => { + if cfg!(feature = "business_profile_routing") { + ( + Some(payout_data.payout_attempt.profile_id.clone()), + business_profile.payout_routing_algorithm.clone(), + ) + } else { + (None, merchant_account.payout_routing_algorithm.clone()) + } + } }; let algorithm_ref = routing_algorithm @@ -2941,7 +2966,7 @@ where state, &merchant_account.merchant_id, algorithm_ref, - payment_data, + transaction_data, ) .await .change_context(errors::ApiErrorResponse::InternalServerError)?; @@ -2951,10 +2976,10 @@ where key_store, merchant_account.modified_at.assume_utc().unix_timestamp(), connectors, - payment_data, + transaction_data, eligible_connectors, #[cfg(feature = "business_profile_routing")] - payment_data.payment_intent.profile_id.clone(), + profile_id, ) .await .change_context(errors::ApiErrorResponse::InternalServerError) diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 96cd6561519..bf64d3ceacb 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -32,11 +32,14 @@ use rand::{ }; use rustc_hash::FxHashMap; +#[cfg(feature = "payouts")] +use crate::core::payouts; #[cfg(not(feature = "business_profile_routing"))] use crate::utils::StringExt; use crate::{ core::{ - errors as oss_errors, errors, payments as payments_oss, routing::helpers as routing_helpers, + errors, errors as oss_errors, payments as payments_oss, + routing::{self, helpers as routing_helpers}, }, logger, types::{ @@ -95,6 +98,64 @@ impl Default for MerchantAccountRoutingAlgorithm { } } +#[cfg(feature = "payouts")] +pub fn make_dsl_input_for_payouts( + payout_data: &payouts::PayoutData, +) -> RoutingResult { + use crate::types::transformers::ForeignFrom; + let mandate = dsl_inputs::MandateData { + mandate_acceptance_type: None, + mandate_type: None, + payment_type: None, + }; + let metadata = payout_data + .payouts + .metadata + .clone() + .map(|val| val.parse_value("routing_parameters")) + .transpose() + .change_context(errors::RoutingError::MetadataParsingError) + .attach_printable("Unable to parse routing_parameters from metadata of payouts") + .unwrap_or_else(|err| { + logger::error!(error=?err); + None + }); + let payment = dsl_inputs::PaymentInput { + amount: payout_data.payouts.amount, + card_bin: None, + currency: payout_data.payouts.destination_currency, + authentication_type: None, + capture_method: None, + business_country: payout_data + .payout_attempt + .business_country + .map(api_enums::Country::from_alpha2), + billing_country: payout_data + .billing_address + .as_ref() + .and_then(|bic| bic.country) + .map(api_enums::Country::from_alpha2), + business_label: payout_data.payout_attempt.business_label.clone(), + setup_future_usage: None, + }; + let payment_method = dsl_inputs::PaymentMethodInput { + payment_method: Some(api_enums::PaymentMethod::foreign_from( + payout_data.payouts.payout_type, + )), + payment_method_type: payout_data + .payout_method_data + .clone() + .map(api_enums::PaymentMethodType::foreign_from), + card_network: None, + }; + Ok(dsl_inputs::BackendInput { + mandate, + metadata, + payment, + payment_method, + }) +} + pub fn make_dsl_input( payment_data: &payments_oss::PaymentData, ) -> RoutingResult @@ -206,8 +267,22 @@ pub async fn perform_static_routing_v1( state: &AppState, merchant_id: &str, algorithm_ref: routing_types::RoutingAlgorithmRef, - payment_data: &mut payments_oss::PaymentData, + transaction_data: &routing::TransactionData<'_, F>, ) -> RoutingResult> { + #[cfg(any( + feature = "profile_specific_fallback_routing", + feature = "business_profile_routing" + ))] + let profile_id = match transaction_data { + routing::TransactionData::Payment(payment_data) => payment_data + .payment_intent + .profile_id + .as_ref() + .get_required_value("profile_id") + .change_context(errors::RoutingError::ProfileIdMissing)?, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(payout_data) => &payout_data.payout_attempt.profile_id, + }; let algorithm_id = if let Some(id) = algorithm_ref.algorithm_id { id } else { @@ -216,14 +291,8 @@ pub async fn perform_static_routing_v1( #[cfg(not(feature = "profile_specific_fallback_routing"))] merchant_id, #[cfg(feature = "profile_specific_fallback_routing")] - { - payment_data - .payment_intent - .profile_id - .as_ref() - .get_required_value("profile_id") - .change_context(errors::RoutingError::ProfileIdMissing)? - }, + profile_id, + &api_enums::TransactionType::from(transaction_data), ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; @@ -236,7 +305,8 @@ pub async fn perform_static_routing_v1( algorithm_ref.timestamp, &algorithm_id, #[cfg(feature = "business_profile_routing")] - payment_data.payment_intent.profile_id.clone(), + Some(profile_id).cloned(), + &api_enums::TransactionType::from(transaction_data), ) .await?; let cached_algorithm: Arc = ROUTING_CACHE @@ -254,7 +324,13 @@ pub async fn perform_static_routing_v1( .change_context(errors::RoutingError::ConnectorSelectionFailed)?, CachedAlgorithm::Advanced(interpreter) => { - let backend_input = make_dsl_input(payment_data)?; + let backend_input = match transaction_data { + routing::TransactionData::Payment(payment_data) => make_dsl_input(payment_data)?, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(payout_data) => { + make_dsl_input_for_payouts(payout_data)? + } + }; execute_dsl_and_get_connector_v1(backend_input, interpreter)? } @@ -267,6 +343,7 @@ async fn ensure_algorithm_cached_v1( timestamp: i64, algorithm_id: &str, #[cfg(feature = "business_profile_routing")] profile_id: Option, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult { #[cfg(feature = "business_profile_routing")] let key = { @@ -275,11 +352,27 @@ async fn ensure_algorithm_cached_v1( .get_required_value("profile_id") .change_context(errors::RoutingError::ProfileIdMissing)?; - format!("routing_config_{merchant_id}_{profile_id}") + match transaction_type { + api_enums::TransactionType::Payment => { + format!("routing_config_{merchant_id}_{profile_id}") + } + #[cfg(feature = "payouts")] + api_enums::TransactionType::Payout => { + format!("routing_config_po_{merchant_id}_{profile_id}") + } + } }; #[cfg(not(feature = "business_profile_routing"))] - let key = format!("dsl_{merchant_id}"); + let key = match transaction_type { + api_enums::TransactionType::Payment => { + format!("dsl_{merchant_id}") + } + #[cfg(feature = "payouts")] + api_enums::TransactionType::Payout => { + format!("dsl_po_{merchant_id}") + } + }; let present = ROUTING_CACHE .present(&key) @@ -308,15 +401,14 @@ async fn ensure_algorithm_cached_v1( Ok(key) } -pub fn perform_straight_through_routing( +pub fn perform_straight_through_routing( algorithm: &routing_types::StraightThroughAlgorithm, - payment_data: &payments_oss::PaymentData, + creds_identifier: Option, ) -> RoutingResult<(Vec, bool)> { Ok(match algorithm { - routing_types::StraightThroughAlgorithm::Single(conn) => ( - vec![(**conn).clone()], - payment_data.creds_identifier.is_none(), - ), + routing_types::StraightThroughAlgorithm::Single(conn) => { + (vec![(**conn).clone()], creds_identifier.is_none()) + } routing_types::StraightThroughAlgorithm::Priority(conns) => (conns.clone(), true), @@ -459,19 +551,31 @@ pub async fn get_merchant_kgraph<'a>( key_store: &domain::MerchantKeyStore, merchant_last_modified: i64, #[cfg(feature = "business_profile_routing")] profile_id: Option, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult>> { + let merchant_id = &key_store.merchant_id; + #[cfg(feature = "business_profile_routing")] let key = { let profile_id = profile_id .clone() .get_required_value("profile_id") .change_context(errors::RoutingError::ProfileIdMissing)?; - - format!("kgraph_{}_{profile_id}", key_store.merchant_id) + match transaction_type { + api_enums::TransactionType::Payment => format!("kgraph_{}_{}", merchant_id, profile_id), + #[cfg(feature = "payouts")] + api_enums::TransactionType::Payout => { + format!("kgraph_po_{}_{}", merchant_id, profile_id) + } + } }; #[cfg(not(feature = "business_profile_routing"))] - let key = format!("kgraph_{}", key_store.merchant_id); + let key = match transaction_type { + api_enums::TransactionType::Payment => format!("kgraph_{}", merchant_id), + #[cfg(feature = "payouts")] + api_enums::TransactionType::Payout => format!("kgraph_po_{}", merchant_id), + }; let kgraph_present = KGRAPH_CACHE .present(&key) @@ -493,6 +597,7 @@ pub async fn get_merchant_kgraph<'a>( key.clone(), #[cfg(feature = "business_profile_routing")] profile_id, + transaction_type, ) .await?; } @@ -512,6 +617,7 @@ pub async fn refresh_kgraph_cache( timestamp: i64, key: String, #[cfg(feature = "business_profile_routing")] profile_id: Option, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult<()> { let mut merchant_connector_accounts = state .store @@ -523,10 +629,20 @@ pub async fn refresh_kgraph_cache( .await .change_context(errors::RoutingError::KgraphCacheRefreshFailed)?; - merchant_connector_accounts.retain(|mca| { - mca.connector_type != storage_enums::ConnectorType::PaymentVas - && mca.connector_type != storage_enums::ConnectorType::PaymentMethodAuth - }); + match transaction_type { + api_enums::TransactionType::Payment => { + merchant_connector_accounts.retain(|mca| { + mca.connector_type != storage_enums::ConnectorType::PaymentVas + && mca.connector_type != storage_enums::ConnectorType::PaymentMethodAuth + && mca.connector_type != storage_enums::ConnectorType::PayoutProcessor + }); + } + #[cfg(feature = "payouts")] + api_enums::TransactionType::Payout => { + merchant_connector_accounts + .retain(|mca| mca.connector_type == storage_enums::ConnectorType::PayoutProcessor); + } + }; #[cfg(feature = "business_profile_routing")] let merchant_connector_accounts = payments_oss::helpers::filter_mca_based_on_business_profile( @@ -554,6 +670,7 @@ pub async fn refresh_kgraph_cache( Ok(()) } +#[allow(clippy::too_many_arguments)] async fn perform_kgraph_filtering( state: &AppState, key_store: &domain::MerchantKeyStore, @@ -562,6 +679,7 @@ async fn perform_kgraph_filtering( backend_input: dsl_inputs::BackendInput, eligible_connectors: Option<&Vec>, #[cfg(feature = "business_profile_routing")] profile_id: Option, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult> { let context = euclid_graph::AnalysisContext::from_dir_values( backend_input @@ -575,6 +693,7 @@ async fn perform_kgraph_filtering( merchant_last_modified, #[cfg(feature = "business_profile_routing")] profile_id, + transaction_type, ) .await?; @@ -607,11 +726,15 @@ pub async fn perform_eligibility_analysis( key_store: &domain::MerchantKeyStore, merchant_last_modified: i64, chosen: Vec, - payment_data: &payments_oss::PaymentData, + transaction_data: &routing::TransactionData<'_, F>, eligible_connectors: Option<&Vec>, #[cfg(feature = "business_profile_routing")] profile_id: Option, ) -> RoutingResult> { - let backend_input = make_dsl_input(payment_data)?; + let backend_input = match transaction_data { + routing::TransactionData::Payment(payment_data) => make_dsl_input(payment_data)?, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(payout_data) => make_dsl_input_for_payouts(payout_data)?, + }; perform_kgraph_filtering( state, @@ -622,6 +745,7 @@ pub async fn perform_eligibility_analysis( eligible_connectors, #[cfg(feature = "business_profile_routing")] profile_id, + &api_enums::TransactionType::from(transaction_data), ) .await } @@ -630,7 +754,7 @@ pub async fn perform_fallback_routing( state: &AppState, key_store: &domain::MerchantKeyStore, merchant_last_modified: i64, - payment_data: &payments_oss::PaymentData, + transaction_data: &routing::TransactionData<'_, F>, eligible_connectors: Option<&Vec>, #[cfg(feature = "business_profile_routing")] profile_id: Option, ) -> RoutingResult> { @@ -639,18 +763,26 @@ pub async fn perform_fallback_routing( #[cfg(not(feature = "profile_specific_fallback_routing"))] &key_store.merchant_id, #[cfg(feature = "profile_specific_fallback_routing")] - { - payment_data + match transaction_data { + routing::TransactionData::Payment(payment_data) => payment_data .payment_intent .profile_id .as_ref() .get_required_value("profile_id") - .change_context(errors::RoutingError::ProfileIdMissing)? + .change_context(errors::RoutingError::ProfileIdMissing)?, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(payout_data) => &payout_data.payout_attempt.profile_id, }, + &api_enums::TransactionType::from(transaction_data), ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; - let backend_input = make_dsl_input(payment_data)?; + + let backend_input = match transaction_data { + routing::TransactionData::Payment(payment_data) => make_dsl_input(payment_data)?, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(payout_data) => make_dsl_input_for_payouts(payout_data)?, + }; perform_kgraph_filtering( state, @@ -661,6 +793,7 @@ pub async fn perform_fallback_routing( eligible_connectors, #[cfg(feature = "business_profile_routing")] profile_id, + &api_enums::TransactionType::from(transaction_data), ) .await } @@ -670,7 +803,7 @@ pub async fn perform_eligibility_analysis_with_fallback( key_store: &domain::MerchantKeyStore, merchant_last_modified: i64, chosen: Vec, - payment_data: &payments_oss::PaymentData, + transaction_data: &routing::TransactionData<'_, F>, eligible_connectors: Option>, #[cfg(feature = "business_profile_routing")] profile_id: Option, ) -> RoutingResult> { @@ -679,7 +812,7 @@ pub async fn perform_eligibility_analysis_with_fallback( key_store, merchant_last_modified, chosen, - payment_data, + transaction_data, eligible_connectors.as_ref(), #[cfg(feature = "business_profile_routing")] profile_id.clone(), @@ -690,7 +823,7 @@ pub async fn perform_eligibility_analysis_with_fallback( state, key_store, merchant_last_modified, - payment_data, + transaction_data, eligible_connectors.as_ref(), #[cfg(feature = "business_profile_routing")] profile_id, @@ -719,6 +852,7 @@ pub async fn perform_eligibility_analysis_with_fallback( pub async fn perform_session_flow_routing( session_input: SessionFlowRoutingInput<'_>, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult> { let mut pm_type_map: FxHashMap> = FxHashMap::default(); @@ -855,7 +989,8 @@ pub async fn perform_session_flow_routing( ))] profile_id: session_input.payment_intent.profile_id.clone(), }; - let maybe_choice = perform_session_routing_for_pm_type(session_pm_input).await?; + let maybe_choice = + perform_session_routing_for_pm_type(session_pm_input, transaction_type).await?; // (connector, sub_label) if let Some(data) = maybe_choice { @@ -876,6 +1011,7 @@ pub async fn perform_session_flow_routing( async fn perform_session_routing_for_pm_type( session_pm_input: SessionRoutingPmTypeInput<'_>, + transaction_type: &api_enums::TransactionType, ) -> RoutingResult)>> { let merchant_id = &session_pm_input.key_store.merchant_id; @@ -889,6 +1025,7 @@ async fn perform_session_routing_for_pm_type( algorithm_id, #[cfg(feature = "business_profile_routing")] session_pm_input.profile_id.clone(), + transaction_type, ) .await?; @@ -923,6 +1060,7 @@ async fn perform_session_routing_for_pm_type( .get_required_value("profile_id") .change_context(errors::RoutingError::ProfileIdMissing)? }, + transaction_type, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)? @@ -939,6 +1077,7 @@ async fn perform_session_routing_for_pm_type( None, #[cfg(feature = "business_profile_routing")] session_pm_input.profile_id.clone(), + transaction_type, ) .await?; @@ -955,6 +1094,7 @@ async fn perform_session_routing_for_pm_type( .get_required_value("profile_id") .change_context(errors::RoutingError::ProfileIdMissing)? }, + transaction_type, ) .await .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; @@ -968,6 +1108,7 @@ async fn perform_session_routing_for_pm_type( None, #[cfg(feature = "business_profile_routing")] session_pm_input.profile_id.clone(), + transaction_type, ) .await?; } diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 8741407d17b..1d21c8a8d7d 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -4,7 +4,7 @@ pub mod validator; use api_models::enums as api_enums; use common_utils::{crypto::Encryptable, ext_traits::ValueExt}; use diesel_models::enums as storage_enums; -use error_stack::{report, ResultExt}; +use error_stack::{report, IntoReport, ResultExt}; use router_env::{instrument, tracing}; use serde_json; @@ -20,7 +20,9 @@ use crate::{ types::{ self, api::{self, payouts}, - domain, storage, + domain, + storage::{self, PaymentRoutingInfo}, + transformers::ForeignTryInto, }, utils::{self, OptionExt}, }; @@ -30,11 +32,12 @@ use crate::{ #[derive(Clone)] pub struct PayoutData { pub billing_address: Option, + pub business_profile: storage::BusinessProfile, pub customer_details: Option, + pub merchant_connector_account: Option, pub payouts: storage::Payouts, pub payout_attempt: storage::PayoutAttempt, pub payout_method_data: Option, - pub merchant_connector_account: Option, pub profile_id: String, } @@ -43,50 +46,111 @@ pub struct PayoutData { pub async fn get_connector_data( state: &AppState, merchant_account: &domain::MerchantAccount, - routed_through: Option, + key_store: &domain::MerchantKeyStore, + connector: Option, routing_algorithm: Option, -) -> RouterResult { - let mut routing_data = storage::PayoutRoutingData { - routed_through, - algorithm: None, - }; + payout_data: &mut PayoutData, + eligible_connectors: Option>, +) -> RouterResult { + let eligible_routable_connectors = eligible_connectors.map(|connectors| { + connectors + .into_iter() + .flat_map(|c| c.foreign_try_into()) + .collect() + }); let connector_choice = helpers::get_default_payout_connector(state, routing_algorithm).await?; let connector_details = match connector_choice { - api::PayoutConnectorChoice::SessionMultiple(session_connectors) => { - api::PayoutConnectorCallType::Multiple(session_connectors) + api::ConnectorChoice::SessionMultiple(_) => { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Invalid connector choice - SessionMultiple")? } - api::PayoutConnectorChoice::StraightThrough(straight_through) => { - let request_straight_through: Option = - Some(straight_through) - .map(|val| val.parse_value("StraightThroughAlgorithm")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Invalid straight through routing rules format")?; + api::ConnectorChoice::StraightThrough(straight_through) => { + let request_straight_through: api::routing::StraightThroughAlgorithm = straight_through + .clone() + .parse_value("StraightThroughAlgorithm") + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Invalid straight through routing rules format")?; + payout_data.payout_attempt.routing_info = Some(straight_through); + let mut routing_data = storage::RoutingData { + routed_through: connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + business_sub_label: payout_data.payout_attempt.business_label.clone(), + algorithm: Some(request_straight_through.clone()), + routing_info: PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }, + }; helpers::decide_payout_connector( state, merchant_account, - request_straight_through, + key_store, + Some(request_straight_through), &mut routing_data, - )? + payout_data, + eligible_routable_connectors, + ) + .await? } - api::PayoutConnectorChoice::Decide => { - helpers::decide_payout_connector(state, merchant_account, None, &mut routing_data)? + api::ConnectorChoice::Decide => { + let mut routing_data = storage::RoutingData { + routed_through: connector, + #[cfg(feature = "connector_choice_mca_id")] + merchant_connector_id: None, + #[cfg(not(feature = "connector_choice_mca_id"))] + business_sub_label: payout_data.payout_attempt.business_label.clone(), + algorithm: None, + routing_info: PaymentRoutingInfo { + algorithm: None, + pre_routing_results: None, + }, + }; + helpers::decide_payout_connector( + state, + merchant_account, + key_store, + None, + &mut routing_data, + payout_data, + eligible_routable_connectors, + ) + .await? } }; let connector_data = match connector_details { - api::PayoutConnectorCallType::Single(connector) => connector, - - api::PayoutConnectorCallType::Multiple(connectors) => { - // TODO: route through actual multiple connectors. - connectors.first().map_or( - Err(errors::ApiErrorResponse::IncorrectConnectorNameGiven), - |c| Ok(c.connector.to_owned()), - )? + api::ConnectorCallType::SessionMultiple(_) => { + Err(errors::ApiErrorResponse::InternalServerError) + .into_report() + .attach_printable("Invalid connector details - SessionMultiple")? + } + api::ConnectorCallType::PreDetermined(connector) => connector, + + api::ConnectorCallType::Retryable(connectors) => { + let mut connectors = connectors.into_iter(); + payments::get_connector_data(&mut connectors)? } }; + // Update connector in DB + payout_data.payout_attempt.connector = Some(connector_data.connector_name.to_string()); + let updated_payout_attempt = storage::PayoutAttemptUpdate::UpdateRouting { + connector: connector_data.connector_name.to_string(), + routing_info: payout_data.payout_attempt.routing_info.clone(), + }; + let db = &*state.store; + db.update_payout_attempt_by_merchant_id_payout_id( + &payout_data.payout_attempt.merchant_id, + &payout_data.payout_attempt.payout_id, + updated_payout_attempt, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error updating routing info in payout_attempt")?; Ok(connector_data) } @@ -98,17 +162,6 @@ pub async fn payouts_create_core( key_store: domain::MerchantKeyStore, req: payouts::PayoutCreateRequest, ) -> RouterResponse { - // Form connector data - let connector_data = get_connector_data( - &state, - &merchant_account, - req.connector - .clone() - .and_then(|c| c.first().map(|c| c.to_string())), - req.routing.clone(), - ) - .await?; - // Validate create request let (payout_id, payout_method_data, profile_id) = validator::validate_create_request(&state, &merchant_account, &req, &key_store).await?; @@ -121,17 +174,28 @@ pub async fn payouts_create_core( &req, &payout_id, &profile_id, - &connector_data.connector_name, payout_method_data.as_ref(), ) .await?; + // Form connector data + let connector_data = get_connector_data( + &state, + &merchant_account, + &key_store, + None, + req.routing.clone(), + &mut payout_data, + req.connector.clone(), + ) + .await?; + call_connector_payout( &state, &merchant_account, &key_store, &req, - connector_data, + &connector_data, &mut payout_data, ) .await @@ -226,21 +290,38 @@ pub async fn payouts_update_core( } } - // Form connector data - let connector_data: api::PayoutConnectorData = api::PayoutConnectorData::get_connector_by_name( - &state.conf.connectors, - &payout_data.payout_attempt.connector, - api::GetToken::Connector, - ) - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to get the connector data")?; + let connector_data = match &payout_attempt.connector { + // Evaluate and fetch connector data + None => { + get_connector_data( + &state, + &merchant_account, + &key_store, + None, + req.routing.clone(), + &mut payout_data, + req.connector.clone(), + ) + .await? + } + + // Use existing connector + Some(connector) => api::ConnectorData::get_payout_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + payout_attempt.merchant_connector_id.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get the connector data")?, + }; call_connector_payout( &state, &merchant_account, &key_store, &req, - connector_data, + &connector_data, &mut payout_data, ) .await @@ -324,13 +405,24 @@ pub async fn payouts_cancel_core( // Trigger connector's cancellation } else { // Form connector data - let connector_data = get_connector_data( - &state, - &merchant_account, - Some(payout_attempt.connector), - None, - ) - .await?; + let connector_data = match &payout_attempt.connector { + Some(connector) => api::ConnectorData::get_payout_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + payout_attempt.merchant_connector_id.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get the connector data")?, + _ => Err(errors::ApplicationError::InvalidConfigurationValueError( + "Connector not found in payout_attempt - should not reach here".to_string(), + )) + .into_report() + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "connector", + }) + .attach_printable("Connector not found for payout cancellation")?, + }; payout_data = cancel_payout( &state, @@ -385,13 +477,24 @@ pub async fn payouts_fulfill_core( } // Form connector data - let connector_data = get_connector_data( - &state, - &merchant_account, - Some(payout_attempt.connector.clone()), - None, - ) - .await?; + let connector_data = match &payout_attempt.connector { + Some(connector) => api::ConnectorData::get_payout_connector_by_name( + &state.conf.connectors, + connector, + api::GetToken::Connector, + payout_attempt.merchant_connector_id.clone(), + ) + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to get the connector data")?, + _ => Err(errors::ApplicationError::InvalidConfigurationValueError( + "Connector not found in payout_attempt - should not reach here.".to_string(), + )) + .into_report() + .change_context(errors::ApiErrorResponse::MissingRequiredField { + field_name: "connector", + }) + .attach_printable("Connector not found for payout fulfillment")?, + }; // Trigger fulfillment payout_data.payout_method_data = Some( @@ -443,7 +546,7 @@ pub async fn call_connector_payout( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, - connector_data: api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResponse { let payout_attempt = &payout_data.payout_attempt.to_owned(); @@ -475,7 +578,7 @@ pub async fn call_connector_payout( merchant_account, key_store, req, - &connector_data, + connector_data, payout_data, ) .await @@ -505,7 +608,7 @@ pub async fn call_connector_payout( merchant_account, key_store, req, - &connector_data, + connector_data, payout_data, ) .await @@ -517,7 +620,7 @@ pub async fn call_connector_payout( merchant_account, key_store, req, - &connector_data, + connector_data, payout_data, ) .await @@ -533,7 +636,7 @@ pub async fn call_connector_payout( merchant_account, key_store, req, - &connector_data, + connector_data, payout_data, ) .await @@ -549,7 +652,7 @@ pub async fn call_connector_payout( merchant_account, key_store, &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), - &connector_data, + connector_data, payout_data, ) .await @@ -571,7 +674,7 @@ pub async fn create_recipient( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, - connector_data: &api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { let customer_details = payout_data.customer_details.to_owned(); @@ -656,7 +759,7 @@ pub async fn check_payout_eligibility( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, - connector_data: &api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { // 1. Form Router data @@ -756,7 +859,7 @@ pub async fn create_payout( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutCreateRequest, - connector_data: &api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { // 1. Form Router data @@ -862,7 +965,7 @@ pub async fn cancel_payout( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutRequest, - connector_data: &api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { // 1. Form Router data @@ -954,7 +1057,7 @@ pub async fn fulfill_payout( merchant_account: &domain::MerchantAccount, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutRequest, - connector_data: &api::PayoutConnectorData, + connector_data: &api::ConnectorData, payout_data: &mut PayoutData, ) -> RouterResult { // 1. Form Router data @@ -1104,7 +1207,7 @@ pub async fn response_handler( merchant_id: merchant_account.merchant_id.to_owned(), amount: payouts.amount.to_owned(), currency: payouts.destination_currency.to_owned(), - connector: Some(payout_attempt.connector.to_owned()), + connector: payout_attempt.connector.to_owned(), payout_type: payouts.payout_type.to_owned(), billing: address, customer_id, @@ -1139,7 +1242,6 @@ pub async fn payout_create_db_entries( req: &payouts::PayoutCreateRequest, payout_id: &String, profile_id: &String, - connector_name: &api_enums::PayoutConnectors, stored_payout_method_data: Option<&payouts::PayoutMethodData>, ) -> RouterResult { let db = &*state.store; @@ -1247,7 +1349,6 @@ pub async fn payout_create_db_entries( .set_customer_id(customer_id.to_owned()) .set_merchant_id(merchant_id.to_owned()) .set_address_id(address_id.to_owned()) - .set_connector(connector_name.to_string()) .set_status(status) .set_business_country(req.business_country.to_owned()) .set_business_label(req.business_label.to_owned()) @@ -1264,10 +1365,16 @@ pub async fn payout_create_db_entries( }) .attach_printable("Error inserting payout_attempt in db")?; + // Validate whether profile_id passed in request is valid and is linked to the merchant + let business_profile = + validate_and_get_business_profile(state, profile_id, merchant_id).await?; + // Make PayoutData Ok(PayoutData { billing_address, + business_profile, customer_details: customer, + merchant_connector_account: None, payouts, payout_attempt, payout_method_data: req @@ -1275,7 +1382,6 @@ pub async fn payout_create_db_entries( .as_ref() .cloned() .or(stored_payout_method_data.cloned()), - merchant_connector_account: None, profile_id: profile_id.to_owned(), }) } @@ -1328,8 +1434,13 @@ pub async fn make_payout_data( let profile_id = payout_attempt.profile_id.clone(); + // Validate whether profile_id passed in request is valid and is linked to the merchant + let business_profile = + validate_and_get_business_profile(state, &profile_id, merchant_id).await?; + Ok(PayoutData { billing_address, + business_profile, customer_details, payouts, payout_attempt, @@ -1338,3 +1449,22 @@ pub async fn make_payout_data( profile_id, }) } + +async fn validate_and_get_business_profile( + state: &AppState, + profile_id: &String, + merchant_id: &str, +) -> RouterResult { + let db = &*state.store; + if let Some(business_profile) = + core_utils::validate_and_get_business_profile(db, Some(profile_id), merchant_id).await? + { + Ok(business_profile) + } else { + db.find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_string(), + }) + } +} diff --git a/crates/router/src/core/payouts/helpers.rs b/crates/router/src/core/payouts/helpers.rs index 7fb6fa3bde6..6e1d3f05f71 100644 --- a/crates/router/src/core/payouts/helpers.rs +++ b/crates/router/src/core/payouts/helpers.rs @@ -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, - routing_data: &mut storage::PayoutRoutingData, -) -> RouterResult { - 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, + routing_data: &mut storage::RoutingData, + payout_data: &mut PayoutData, + eligible_connectors: Option>, +) -> RouterResult { + // 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::, _>>() + .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::, _>>() + .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::("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, -) -> CustomResult { +) -> CustomResult { 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, 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); - - 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) + 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) + } + } + _ => (false, None), } } diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs index c544a987ba8..31a2e05dd79 100644 --- a/crates/router/src/core/routing.rs +++ b/crates/router/src/core/routing.rs @@ -1,9 +1,12 @@ pub mod helpers; pub mod transformers; -use api_models::routing::{self as routing_types, RoutingAlgorithmId}; #[cfg(feature = "business_profile_routing")] use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +use api_models::{ + enums, + routing::{self as routing_types, RoutingAlgorithmId}, +}; #[cfg(not(feature = "business_profile_routing"))] use common_utils::ext_traits::{Encode, StringExt}; #[cfg(not(feature = "business_profile_routing"))] @@ -13,6 +16,9 @@ use diesel_models::routing_algorithm::RoutingAlgorithm; use error_stack::{IntoReport, ResultExt}; use rustc_hash::FxHashSet; +use super::payments; +#[cfg(feature = "payouts")] +use super::payouts; #[cfg(feature = "business_profile_routing")] use crate::types::transformers::{ForeignInto, ForeignTryInto}; use crate::{ @@ -30,18 +36,30 @@ use crate::{core::errors, services::api as service_api, types::storage}; #[cfg(feature = "business_profile_routing")] use crate::{errors, services::api as service_api}; +#[derive(Clone)] +pub enum TransactionData<'a, F> +where + F: Clone, +{ + Payment(&'a payments::PaymentData), + #[cfg(feature = "payouts")] + Payout(&'a payouts::PayoutData), +} + pub async fn retrieve_merchant_routing_dictionary( state: AppState, merchant_account: domain::MerchantAccount, #[cfg(feature = "business_profile_routing")] query_params: RoutingRetrieveQuery, + #[cfg(feature = "business_profile_routing")] transaction_type: &enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_MERCHANT_DICTIONARY_RETRIEVE.add(&metrics::CONTEXT, 1, &[]); #[cfg(feature = "business_profile_routing")] { let routing_metadata = state .store - .list_routing_algorithm_metadata_by_merchant_id( + .list_routing_algorithm_metadata_by_merchant_id_transaction_type( &merchant_account.merchant_id, + transaction_type, i64::from(query_params.limit.unwrap_or_default()), i64::from(query_params.offset.unwrap_or_default()), ) @@ -80,6 +98,7 @@ pub async fn create_routing_config( merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, request: routing_types::RoutingConfigRequest, + transaction_type: &enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); @@ -148,6 +167,7 @@ pub async fn create_routing_config( algorithm_data: serde_json::json!(algorithm), created_at: timestamp, modified_at: timestamp, + algorithm_for: transaction_type.to_owned(), }; let record = db .insert_routing_algorithm(algo) @@ -197,6 +217,7 @@ pub async fn create_routing_config( description: description.clone(), created_at: timestamp, modified_at: timestamp, + algorithm_for: Some(*transaction_type), }; merchant_dictionary.records.push(new_record.clone()); @@ -233,6 +254,7 @@ pub async fn link_routing_config( merchant_account: domain::MerchantAccount, #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, algorithm_id: String, + transaction_type: &enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_LINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); @@ -257,14 +279,25 @@ pub async fn link_routing_config( id: routing_algorithm.profile_id.clone(), })?; - let mut routing_ref: routing_types::RoutingAlgorithmRef = business_profile - .routing_algorithm - .clone() - .map(|val| val.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize routing algorithm ref from merchant account")? - .unwrap_or_default(); + let mut routing_ref: routing_types::RoutingAlgorithmRef = match transaction_type { + enums::TransactionType::Payment => business_profile.routing_algorithm.clone(), + #[cfg(feature = "payouts")] + enums::TransactionType::Payout => business_profile.payout_routing_algorithm.clone(), + } + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); + + utils::when(routing_algorithm.algorithm_for != *transaction_type, || { + Err(errors::ApiErrorResponse::PreconditionFailed { + message: format!( + "Cannot use {}'s routing algorithm for {} operation", + routing_algorithm.algorithm_for, transaction_type + ), + }) + })?; utils::when( routing_ref.algorithm_id == Some(algorithm_id.clone()), @@ -277,8 +310,13 @@ pub async fn link_routing_config( )?; routing_ref.update_algorithm_id(algorithm_id); - helpers::update_business_profile_active_algorithm_ref(db, business_profile, routing_ref) - .await?; + helpers::update_business_profile_active_algorithm_ref( + db, + business_profile, + routing_ref, + transaction_type, + ) + .await?; metrics::ROUTING_LINK_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); Ok(service_api::ApplicationResponse::Json( @@ -288,14 +326,16 @@ pub async fn link_routing_config( #[cfg(not(feature = "business_profile_routing"))] { - let mut routing_ref: routing_types::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|val| val.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize routing algorithm ref from merchant account")? - .unwrap_or_default(); + let mut routing_ref: routing_types::RoutingAlgorithmRef = match transaction_type { + enums::TransactionType::Payment => merchant_account.routing_algorithm.clone(), + #[cfg(feature = "payouts")] + enums::TransactionType::Payout => merchant_account.payout_routing_algorithm.clone(), + } + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); utils::when( routing_ref.algorithm_id == Some(algorithm_id.clone()), @@ -402,6 +442,9 @@ pub async fn retrieve_routing_config( algorithm, created_at: record.created_at, modified_at: record.modified_at, + algorithm_for: record + .algorithm_for + .unwrap_or(enums::TransactionType::Payment), }; metrics::ROUTING_RETRIEVE_CONFIG_SUCCESS_RESPONSE.add(&metrics::CONTEXT, 1, &[]); @@ -413,6 +456,7 @@ pub async fn unlink_routing_config( merchant_account: domain::MerchantAccount, #[cfg(not(feature = "business_profile_routing"))] key_store: domain::MerchantKeyStore, #[cfg(feature = "business_profile_routing")] request: routing_types::RoutingConfigRequest, + transaction_type: &enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_UNLINK_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); @@ -433,16 +477,20 @@ pub async fn unlink_routing_config( .await?; match business_profile { Some(business_profile) => { - let routing_algo_ref: routing_types::RoutingAlgorithmRef = business_profile - .routing_algorithm - .clone() - .map(|val| val.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable( - "unable to deserialize routing algorithm ref from merchant account", - )? - .unwrap_or_default(); + let routing_algo_ref: routing_types::RoutingAlgorithmRef = match transaction_type { + enums::TransactionType::Payment => business_profile.routing_algorithm.clone(), + #[cfg(feature = "payouts")] + enums::TransactionType::Payout => { + business_profile.payout_routing_algorithm.clone() + } + } + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to deserialize routing algorithm ref from merchant account", + )? + .unwrap_or_default(); let timestamp = common_utils::date_time::now_unix_timestamp(); @@ -468,6 +516,7 @@ pub async fn unlink_routing_config( db, business_profile, routing_algorithm, + transaction_type, ) .await?; @@ -496,14 +545,16 @@ pub async fn unlink_routing_config( let mut merchant_dictionary = helpers::get_merchant_routing_dictionary(db, &merchant_account.merchant_id).await?; - let routing_algo_ref: routing_types::RoutingAlgorithmRef = merchant_account - .routing_algorithm - .clone() - .map(|val| val.parse_value("RoutingAlgorithmRef")) - .transpose() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("unable to deserialize routing algorithm ref from merchant account")? - .unwrap_or_default(); + let routing_algo_ref: routing_types::RoutingAlgorithmRef = match transaction_type { + enums::TransactionType::Payment => merchant_account.routing_algorithm.clone(), + #[cfg(feature = "payouts")] + enums::TransactionType::Payout => merchant_account.payout_routing_algorithm.clone(), + } + .map(|val| val.parse_value("RoutingAlgorithmRef")) + .transpose() + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("unable to deserialize routing algorithm ref from merchant account")? + .unwrap_or_default(); let timestamp = common_utils::date_time::now_unix_timestamp(); utils::when(routing_algo_ref.algorithm_id.is_none(), || { @@ -593,11 +644,13 @@ pub async fn update_default_routing_config( state: AppState, merchant_account: domain::MerchantAccount, updated_config: Vec, + transaction_type: &enums::TransactionType, ) -> RouterResponse> { metrics::ROUTING_UPDATE_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); let default_config = - helpers::get_merchant_default_config(db, &merchant_account.merchant_id).await?; + helpers::get_merchant_default_config(db, &merchant_account.merchant_id, transaction_type) + .await?; utils::when(default_config.len() != updated_config.len(), || { Err(errors::ApiErrorResponse::PreconditionFailed { @@ -630,6 +683,7 @@ pub async fn update_default_routing_config( db, &merchant_account.merchant_id, updated_config.clone(), + transaction_type, ) .await?; @@ -640,11 +694,12 @@ pub async fn update_default_routing_config( pub async fn retrieve_default_routing_config( state: AppState, merchant_account: domain::MerchantAccount, + transaction_type: &enums::TransactionType, ) -> RouterResponse> { metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); - helpers::get_merchant_default_config(db, &merchant_account.merchant_id) + helpers::get_merchant_default_config(db, &merchant_account.merchant_id, transaction_type) .await .map(|conn_choice| { metrics::ROUTING_RETRIEVE_DEFAULT_CONFIG_SUCCESS_RESPONSE.add( @@ -747,6 +802,9 @@ pub async fn retrieve_linked_routing_config( algorithm: the_algorithm, created_at: record.created_at, modified_at: record.modified_at, + algorithm_for: record + .algorithm_for + .unwrap_or(enums::TransactionType::Payment), }) } else { None @@ -764,6 +822,7 @@ pub async fn retrieve_linked_routing_config( pub async fn retrieve_default_routing_config_for_profiles( state: AppState, merchant_account: domain::MerchantAccount, + transaction_type: &enums::TransactionType, ) -> RouterResponse> { metrics::ROUTING_RETRIEVE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); @@ -776,7 +835,7 @@ pub async fn retrieve_default_routing_config_for_profiles( let retrieve_config_futures = all_profiles .iter() - .map(|prof| helpers::get_merchant_default_config(db, &prof.profile_id)) + .map(|prof| helpers::get_merchant_default_config(db, &prof.profile_id, transaction_type)) .collect::>(); let configs = futures::future::join_all(retrieve_config_futures) @@ -804,6 +863,7 @@ pub async fn update_default_routing_config_for_profile( merchant_account: domain::MerchantAccount, updated_config: Vec, profile_id: String, + transaction_type: &enums::TransactionType, ) -> RouterResponse { metrics::ROUTING_UPDATE_CONFIG_FOR_PROFILE.add(&metrics::CONTEXT, 1, &[]); let db = state.store.as_ref(); @@ -817,7 +877,8 @@ pub async fn update_default_routing_config_for_profile( .get_required_value("BusinessProfile") .change_context(errors::ApiErrorResponse::BusinessProfileNotFound { id: profile_id })?; let default_config = - helpers::get_merchant_default_config(db, &business_profile.profile_id).await?; + helpers::get_merchant_default_config(db, &business_profile.profile_id, transaction_type) + .await?; utils::when(default_config.len() != updated_config.len(), || { Err(errors::ApiErrorResponse::PreconditionFailed { @@ -868,6 +929,7 @@ pub async fn update_default_routing_config_for_profile( db, &business_profile.profile_id, updated_config.clone(), + transaction_type, ) .await?; diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 71010e9bf8b..e1edf7b4a6e 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -71,8 +71,9 @@ pub async fn get_merchant_routing_dictionary( pub async fn get_merchant_default_config( db: &dyn StorageInterface, merchant_id: &str, + transaction_type: &storage::enums::TransactionType, ) -> RouterResult> { - let key = get_default_config_key(merchant_id); + let key = get_default_config_key(merchant_id, transaction_type); let maybe_config = db.find_config_by_key(&key).await; match maybe_config { @@ -116,8 +117,9 @@ pub async fn update_merchant_default_config( db: &dyn StorageInterface, merchant_id: &str, connectors: Vec, + transaction_type: &storage::enums::TransactionType, ) -> RouterResult<()> { - let key = get_default_config_key(merchant_id); + let key = get_default_config_key(merchant_id, transaction_type); let config_str = connectors .encode_to_string_of_json() .change_context(errors::ApiErrorResponse::InternalServerError) @@ -230,12 +232,19 @@ pub async fn update_business_profile_active_algorithm_ref( db: &dyn StorageInterface, current_business_profile: BusinessProfile, algorithm_id: routing_types::RoutingAlgorithmRef, + transaction_type: &storage::enums::TransactionType, ) -> RouterResult<()> { let ref_val = algorithm_id .encode_to_value() .change_context(errors::ApiErrorResponse::InternalServerError) .attach_printable("Failed to convert routing ref to value")?; + let (routing_algorithm, payout_routing_algorithm) = match transaction_type { + storage::enums::TransactionType::Payment => (Some(ref_val), None), + #[cfg(feature = "payouts")] + storage::enums::TransactionType::Payout => (None, Some(ref_val)), + }; + let business_profile_update = BusinessProfileUpdateInternal { profile_name: None, return_url: None, @@ -244,10 +253,10 @@ pub async fn update_business_profile_active_algorithm_ref( redirect_to_merchant_with_http_post: None, webhook_details: None, metadata: None, - routing_algorithm: Some(ref_val), + routing_algorithm, intent_fulfillment_time: None, frm_routing_algorithm: None, - payout_routing_algorithm: None, + payout_routing_algorithm, applepay_verified_domains: None, modified_at: None, is_recon_enabled: None, @@ -461,8 +470,15 @@ pub fn get_pg_agnostic_mandate_config_key(merchant_id: &str) -> String { /// Provides the identifier for the specific merchant's default_config #[inline(always)] -pub fn get_default_config_key(merchant_id: &str) -> String { - format!("routing_default_{merchant_id}") +pub fn get_default_config_key( + merchant_id: &str, + transaction_type: &storage::enums::TransactionType, +) -> String { + match transaction_type { + storage::enums::TransactionType::Payment => format!("routing_default_{merchant_id}"), + #[cfg(feature = "payouts")] + storage::enums::TransactionType::Payout => format!("routing_default_po_{merchant_id}"), + } } pub fn get_payment_config_routing_id(merchant_id: &str) -> String { format!("payment_config_id_{merchant_id}") diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs index e5f1f1e1d5f..4feca317a55 100644 --- a/crates/router/src/core/routing/transformers.rs +++ b/crates/router/src/core/routing/transformers.rs @@ -9,7 +9,7 @@ use diesel_models::{ }; use crate::{ - core::errors, + core::{errors, routing}, types::transformers::{ForeignFrom, ForeignInto, ForeignTryFrom}, }; @@ -20,11 +20,11 @@ impl ForeignFrom for RoutingDictionaryRecord { #[cfg(feature = "business_profile_routing")] profile_id: value.profile_id, name: value.name, - kind: value.kind.foreign_into(), description: value.description.unwrap_or_default(), created_at: value.created_at.assume_utc().unix_timestamp(), modified_at: value.modified_at.assume_utc().unix_timestamp(), + algorithm_for: Some(value.algorithm_for), } } } @@ -40,6 +40,7 @@ impl ForeignFrom for RoutingDictionaryRecord { description: value.description.unwrap_or_default(), created_at: value.created_at.assume_utc().unix_timestamp(), modified_at: value.modified_at.assume_utc().unix_timestamp(), + algorithm_for: Some(value.algorithm_for), } } } @@ -59,6 +60,7 @@ impl ForeignTryFrom for MerchantRoutingAlgorithm { .parse_value::("RoutingAlgorithm")?, created_at: value.created_at.assume_utc().unix_timestamp(), modified_at: value.modified_at.assume_utc().unix_timestamp(), + algorithm_for: value.algorithm_for, }) } } @@ -84,3 +86,13 @@ impl ForeignFrom for storage_enums::RoutingAlgorithmKind { } } } + +impl From<&routing::TransactionData<'_, F>> for storage_enums::TransactionType { + fn from(value: &routing::TransactionData<'_, F>) -> Self { + match value { + routing::TransactionData::Payment(_) => Self::Payment, + #[cfg(feature = "payouts")] + routing::TransactionData::Payout(_) => Self::Payout, + } + } +} diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index ef8dddde955..06a132a57e0 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -116,7 +116,14 @@ pub async fn construct_payout_router_data<'a, F>( let payouts = &payout_data.payouts; let payout_attempt = &payout_data.payout_attempt; let customer_details = &payout_data.customer_details; - let connector_label = format!("{}_{}", payout_data.profile_id, payout_attempt.connector); + let connector_name = payout_attempt + .connector + .clone() + .get_required_value("connector") + .change_context(errors::ApiErrorResponse::InvalidRequestData { + message: "Could not decide to route the connector".to_string(), + })?; + let connector_label = format!("{}_{}", payout_data.profile_id, connector_name); let connector_customer_id = customer_details .as_ref() .and_then(|c| c.connector_customer.as_ref()) diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 213417b1318..a18d18e61b8 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -1801,6 +1801,23 @@ impl RoutingAlgorithmInterface for KafkaStore { .list_routing_algorithm_metadata_by_merchant_id(merchant_id, limit, offset) .await } + + async fn list_routing_algorithm_metadata_by_merchant_id_transaction_type( + &self, + merchant_id: &str, + transaction_type: &enums::TransactionType, + limit: i64, + offset: i64, + ) -> CustomResult, errors::StorageError> { + self.diesel_store + .list_routing_algorithm_metadata_by_merchant_id_transaction_type( + merchant_id, + transaction_type, + limit, + offset, + ) + .await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/routing_algorithm.rs b/crates/router/src/db/routing_algorithm.rs index 58550b2f01f..9970d1d9c9e 100644 --- a/crates/router/src/db/routing_algorithm.rs +++ b/crates/router/src/db/routing_algorithm.rs @@ -48,6 +48,14 @@ pub trait RoutingAlgorithmInterface { limit: i64, offset: i64, ) -> StorageResult>; + + async fn list_routing_algorithm_metadata_by_merchant_id_transaction_type( + &self, + merchant_id: &str, + transaction_type: &common_enums::TransactionType, + limit: i64, + offset: i64, + ) -> StorageResult>; } #[async_trait::async_trait] @@ -144,6 +152,26 @@ impl RoutingAlgorithmInterface for Store { .map_err(Into::into) .into_report() } + + async fn list_routing_algorithm_metadata_by_merchant_id_transaction_type( + &self, + merchant_id: &str, + transaction_type: &common_enums::TransactionType, + limit: i64, + offset: i64, + ) -> StorageResult> { + let conn = connection::pg_connection_write(self).await?; + routing_storage::RoutingAlgorithm::list_metadata_by_merchant_id_transaction_type( + &conn, + merchant_id, + transaction_type, + limit, + offset, + ) + .await + .map_err(Into::into) + .into_report() + } } #[async_trait::async_trait] @@ -196,4 +224,14 @@ impl RoutingAlgorithmInterface for MockDb { ) -> StorageResult> { Err(errors::StorageError::MockDbError)? } + + async fn list_routing_algorithm_metadata_by_merchant_id_transaction_type( + &self, + _merchant_id: &str, + _transaction_type: &common_enums::TransactionType, + _limit: i64, + _offset: i64, + ) -> StorageResult> { + Err(errors::StorageError::MockDbError)? + } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 41881c60ff7..8c6355e73e2 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -6,6 +6,10 @@ use actix_web::{web, Scope}; any(feature = "hashicorp-vault", feature = "aws_kms") ))] use analytics::AnalyticsConfig; +#[cfg(all(feature = "business_profile_routing", feature = "olap"))] +use api_models::routing::RoutingRetrieveQuery; +#[cfg(feature = "olap")] +use common_enums::TransactionType; #[cfg(feature = "aws_kms")] use external_services::aws_kms::{self, decrypt::AwsKmsDecrypt}; #[cfg(feature = "email")] @@ -53,6 +57,7 @@ use crate::routes::recon as recon_routes; use crate::routes::verify_connector::payment_connector_verify; pub use crate::{ configs::settings, + core::routing, db::{StorageImpl, StorageInterface}, events::EventsHandler, routes::cards_info::card_iin_info, @@ -468,7 +473,8 @@ pub struct Routing; #[cfg(feature = "olap")] impl Routing { pub fn server(state: AppState) -> Scope { - web::scope("/routing") + #[allow(unused_mut)] + let mut route = web::scope("/routing") .app_data(web::Data::new(state.clone())) .service( web::resource("/active") @@ -476,18 +482,57 @@ impl Routing { ) .service( web::resource("") - .route(web::get().to(cloud_routing::list_routing_configs)) - .route(web::post().to(cloud_routing::routing_create_config)), + .route( + #[cfg(feature = "business_profile_routing")] + web::get().to(|state, req, path: web::Query| { + cloud_routing::list_routing_configs( + state, + req, + path, + &TransactionType::Payment, + ) + }), + #[cfg(not(feature = "business_profile_routing"))] + web::get().to(cloud_routing::list_routing_configs), + ) + .route(web::post().to(|state, req, payload| { + cloud_routing::routing_create_config( + state, + req, + payload, + &TransactionType::Payment, + ) + })), ) .service( web::resource("/default") - .route(web::get().to(cloud_routing::routing_retrieve_default_config)) - .route(web::post().to(cloud_routing::routing_update_default_config)), - ) - .service( - web::resource("/deactivate") - .route(web::post().to(cloud_routing::routing_unlink_config)), + .route(web::get().to(|state, req| { + cloud_routing::routing_retrieve_default_config( + state, + req, + &TransactionType::Payment, + ) + })) + .route(web::post().to(|state, req, payload| { + cloud_routing::routing_update_default_config( + state, + req, + payload, + &TransactionType::Payment, + ) + })), ) + .service(web::resource("/deactivate").route(web::post().to( + |state, req, #[cfg(feature = "business_profile_routing")] payload| { + cloud_routing::routing_unlink_config( + state, + req, + #[cfg(feature = "business_profile_routing")] + payload, + &TransactionType::Payment, + ) + }, + ))) .service( web::resource("/decision") .route(web::put().to(cloud_routing::upsert_decision_manager_config)) @@ -503,23 +548,140 @@ impl Routing { ), ) .service( - web::resource("/{algorithm_id}") - .route(web::get().to(cloud_routing::routing_retrieve_config)), + web::resource("/default/profile/{profile_id}").route(web::post().to( + |state, req, path, payload| { + cloud_routing::routing_update_default_config_for_profile( + state, + req, + path, + payload, + &TransactionType::Payment, + ) + }, + )), ) .service( - web::resource("/{algorithm_id}/activate") - .route(web::post().to(cloud_routing::routing_link_config)), - ) + web::resource("/default/profile").route(web::get().to(|state, req| { + cloud_routing::routing_retrieve_default_config_for_profiles( + state, + req, + &TransactionType::Payment, + ) + })), + ); + + #[cfg(feature = "payouts")] + { + route = route + .service( + web::resource("/payouts") + .route( + #[cfg(feature = "business_profile_routing")] + web::get().to(|state, req, path: web::Query| { + cloud_routing::list_routing_configs( + state, + req, + #[cfg(feature = "business_profile_routing")] + path, + #[cfg(feature = "business_profile_routing")] + &TransactionType::Payout, + ) + }), + #[cfg(not(feature = "business_profile_routing"))] + web::get().to(cloud_routing::list_routing_configs), + ) + .route(web::post().to(|state, req, payload| { + cloud_routing::routing_create_config( + state, + req, + payload, + &TransactionType::Payout, + ) + })), + ) + .service( + web::resource("/payouts/default") + .route(web::get().to(|state, req| { + cloud_routing::routing_retrieve_default_config( + state, + req, + &TransactionType::Payout, + ) + })) + .route(web::post().to(|state, req, payload| { + cloud_routing::routing_update_default_config( + state, + req, + payload, + &TransactionType::Payout, + ) + })), + ) + .service( + web::resource("/payouts/{algorithm_id}/activate").route(web::post().to( + |state, req, path| { + cloud_routing::routing_link_config( + state, + req, + path, + &TransactionType::Payout, + ) + }, + )), + ) + .service(web::resource("/payouts/deactivate").route(web::post().to( + |state, req, #[cfg(feature = "business_profile_routing")] payload| { + cloud_routing::routing_unlink_config( + state, + req, + #[cfg(feature = "business_profile_routing")] + payload, + &TransactionType::Payout, + ) + }, + ))) + .service( + web::resource("/payouts/default/profile/{profile_id}").route(web::post().to( + |state, req, path, payload| { + cloud_routing::routing_update_default_config_for_profile( + state, + req, + path, + payload, + &TransactionType::Payout, + ) + }, + )), + ) + .service( + web::resource("/payouts/default/profile").route(web::get().to(|state, req| { + cloud_routing::routing_retrieve_default_config_for_profiles( + state, + req, + &TransactionType::Payout, + ) + })), + ); + } + + route = route .service( - web::resource("/default/profile/{profile_id}").route( - web::post().to(cloud_routing::routing_update_default_config_for_profile), - ), + web::resource("/{algorithm_id}") + .route(web::get().to(cloud_routing::routing_retrieve_config)), ) .service( - web::resource("/default/profile").route( - web::get().to(cloud_routing::routing_retrieve_default_config_for_profiles), - ), - ) + web::resource("/{algorithm_id}/activate").route(web::post().to( + |state, req, path| { + cloud_routing::routing_link_config( + state, + req, + path, + &TransactionType::Payment, + ) + }, + )), + ); + route } } diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs index 0f139e93614..80ff1590d55 100644 --- a/crates/router/src/routes/routing.rs +++ b/crates/router/src/routes/routing.rs @@ -3,9 +3,9 @@ //! Functions that are used to perform the api level configuration, retrieval, updation //! of Routing configs. use actix_web::{web, HttpRequest, Responder}; -use api_models::routing as routing_types; #[cfg(feature = "business_profile_routing")] use api_models::routing::{RoutingRetrieveLinkQuery, RoutingRetrieveQuery}; +use api_models::{enums, routing as routing_types}; use router_env::{ tracing::{self, instrument}, Flow, @@ -23,6 +23,7 @@ pub async fn routing_create_config( state: web::Data, req: HttpRequest, json_payload: web::Json, + transaction_type: &enums::TransactionType, ) -> impl Responder { let flow = Flow::RoutingCreateConfig; Box::pin(oss_api::server_wrap( @@ -31,7 +32,13 @@ pub async fn routing_create_config( &req, json_payload.into_inner(), |state, auth: auth::AuthenticationData, payload| { - routing::create_routing_config(state, auth.merchant_account, auth.key_store, payload) + routing::create_routing_config( + state, + auth.merchant_account, + auth.key_store, + payload, + transaction_type, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -52,6 +59,7 @@ pub async fn routing_link_config( state: web::Data, req: HttpRequest, path: web::Path, + transaction_type: &enums::TransactionType, ) -> impl Responder { let flow = Flow::RoutingLinkConfig; Box::pin(oss_api::server_wrap( @@ -66,6 +74,7 @@ pub async fn routing_link_config( #[cfg(not(feature = "business_profile_routing"))] auth.key_store, algorithm_id.0, + transaction_type, ) }, #[cfg(not(feature = "release"))] @@ -117,6 +126,7 @@ pub async fn list_routing_configs( state: web::Data, req: HttpRequest, #[cfg(feature = "business_profile_routing")] query: web::Query, + #[cfg(feature = "business_profile_routing")] transaction_type: &enums::TransactionType, ) -> impl Responder { #[cfg(feature = "business_profile_routing")] { @@ -131,6 +141,7 @@ pub async fn list_routing_configs( state, auth.merchant_account, query_params, + transaction_type, ) }, #[cfg(not(feature = "release"))] @@ -179,6 +190,7 @@ pub async fn routing_unlink_config( #[cfg(feature = "business_profile_routing")] payload: web::Json< routing_types::RoutingConfigRequest, >, + transaction_type: &enums::TransactionType, ) -> impl Responder { #[cfg(feature = "business_profile_routing")] { @@ -189,7 +201,12 @@ pub async fn routing_unlink_config( &req, payload.into_inner(), |state, auth: auth::AuthenticationData, payload_req| { - routing::unlink_routing_config(state, auth.merchant_account, payload_req) + routing::unlink_routing_config( + state, + auth.merchant_account, + payload_req, + transaction_type, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -213,7 +230,12 @@ pub async fn routing_unlink_config( &req, (), |state, auth: auth::AuthenticationData, _| { - routing::unlink_routing_config(state, auth.merchant_account, auth.key_store) + routing::unlink_routing_config( + state, + auth.merchant_account, + auth.key_store, + transaction_type, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -235,6 +257,7 @@ pub async fn routing_update_default_config( state: web::Data, req: HttpRequest, json_payload: web::Json>, + transaction_type: &enums::TransactionType, ) -> impl Responder { oss_api::server_wrap( Flow::RoutingUpdateDefaultConfig, @@ -242,7 +265,12 @@ pub async fn routing_update_default_config( &req, json_payload.into_inner(), |state, auth: auth::AuthenticationData, updated_config| { - routing::update_default_routing_config(state, auth.merchant_account, updated_config) + routing::update_default_routing_config( + state, + auth.merchant_account, + updated_config, + transaction_type, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -262,6 +290,7 @@ pub async fn routing_update_default_config( pub async fn routing_retrieve_default_config( state: web::Data, req: HttpRequest, + transaction_type: &enums::TransactionType, ) -> impl Responder { oss_api::server_wrap( Flow::RoutingRetrieveDefaultConfig, @@ -269,7 +298,7 @@ pub async fn routing_retrieve_default_config( &req, (), |state, auth: auth::AuthenticationData, _| { - routing::retrieve_default_routing_config(state, auth.merchant_account) + routing::retrieve_default_routing_config(state, auth.merchant_account, transaction_type) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -536,6 +565,7 @@ pub async fn routing_retrieve_linked_config( pub async fn routing_retrieve_default_config_for_profiles( state: web::Data, req: HttpRequest, + transaction_type: &enums::TransactionType, ) -> impl Responder { oss_api::server_wrap( Flow::RoutingRetrieveDefaultConfig, @@ -543,7 +573,11 @@ pub async fn routing_retrieve_default_config_for_profiles( &req, (), |state, auth: auth::AuthenticationData, _| { - routing::retrieve_default_routing_config_for_profiles(state, auth.merchant_account) + routing::retrieve_default_routing_config_for_profiles( + state, + auth.merchant_account, + transaction_type, + ) }, #[cfg(not(feature = "release"))] auth::auth_type( @@ -569,6 +603,7 @@ pub async fn routing_update_default_config_for_profile( req: HttpRequest, path: web::Path, json_payload: web::Json>, + transaction_type: &enums::TransactionType, ) -> impl Responder { let routing_payload_wrapper = routing_types::RoutingPayloadWrapper { updated_config: json_payload.into_inner(), @@ -585,6 +620,7 @@ pub async fn routing_update_default_config_for_profile( auth.merchant_account, wrapper.updated_config, wrapper.profile_id, + transaction_type, ) }, #[cfg(not(feature = "release"))] diff --git a/crates/router/src/types/api.rs b/crates/router/src/types/api.rs index db6cb8236b3..894b3d830dd 100644 --- a/crates/router/src/types/api.rs +++ b/crates/router/src/types/api.rs @@ -222,22 +222,6 @@ pub struct ConnectorData { pub merchant_connector_id: Option, } -#[cfg(feature = "payouts")] -#[derive(Clone)] -pub struct PayoutConnectorData { - pub connector: BoxedConnector, - pub connector_name: api_enums::PayoutConnectors, - pub get_token: GetToken, -} - -#[cfg(feature = "payouts")] -#[derive(Clone)] -pub struct PayoutSessionConnectorData { - pub payment_method_type: api_enums::PaymentMethodType, - pub connector: PayoutConnectorData, - pub business_sub_label: Option, -} - #[derive(Clone)] pub struct SessionConnectorData { pub payment_method_type: api_enums::PaymentMethodType, @@ -279,71 +263,42 @@ pub enum ConnectorChoice { Decide, } -#[cfg(feature = "payouts")] -pub enum PayoutConnectorChoice { - SessionMultiple(Vec), - StraightThrough(serde_json::Value), - Decide, -} - -#[cfg(feature = "payouts")] -#[derive(Clone)] -pub enum PayoutConnectorCallType { - Multiple(Vec), - Single(PayoutConnectorData), -} - -#[cfg(feature = "payouts")] -impl PayoutConnectorData { +impl ConnectorData { pub fn get_connector_by_name( connectors: &Connectors, name: &str, connector_type: GetToken, + connector_id: Option, ) -> CustomResult { let connector = Self::convert_connector(connectors, name)?; - let connector_name = api_enums::PayoutConnectors::from_str(name) + let connector_name = api_enums::Connector::from_str(name) .into_report() .change_context(errors::ConnectorError::InvalidConnectorName) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| { - format!("unable to parse payout connector name {connector:?}") - })?; + .attach_printable_lazy(|| format!("unable to parse connector name {connector:?}"))?; Ok(Self { connector, connector_name, get_token: connector_type, + merchant_connector_id: connector_id, }) } - fn convert_connector( - _connectors: &Connectors, - connector_name: &str, - ) -> CustomResult { - match enums::PayoutConnectors::from_str(connector_name) { - Ok(name) => match name { - enums::PayoutConnectors::Adyen => Ok(Box::new(&connector::Adyen)), - enums::PayoutConnectors::Wise => Ok(Box::new(&connector::Wise)), - }, - Err(_) => Err(report!(errors::ConnectorError::InvalidConnectorName) - .attach_printable(format!("invalid payout connector name: {connector_name}"))) - .change_context(errors::ApiErrorResponse::InternalServerError), - } - } -} - -impl ConnectorData { - pub fn get_connector_by_name( + pub fn get_payout_connector_by_name( connectors: &Connectors, name: &str, connector_type: GetToken, connector_id: Option, ) -> CustomResult { let connector = Self::convert_connector(connectors, name)?; - let connector_name = api_enums::Connector::from_str(name) + let payout_connector_name = api_enums::PayoutConnectors::from_str(name) .into_report() .change_context(errors::ConnectorError::InvalidConnectorName) .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable_lazy(|| format!("unable to parse connector name {connector:?}"))?; + .attach_printable_lazy(|| { + format!("unable to parse payout connector name {connector:?}") + })?; + let connector_name = api_enums::Connector::from(payout_connector_name); Ok(Self { connector, connector_name, diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index bf76a7339a2..6e0f9ab8612 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -1,11 +1,10 @@ pub use api_models::admin::{ - payout_routing_algorithm, BusinessProfileCreate, BusinessProfileResponse, - BusinessProfileUpdate, MerchantAccountCreate, MerchantAccountDeleteResponse, - MerchantAccountResponse, MerchantAccountUpdate, MerchantConnectorCreate, - MerchantConnectorDeleteResponse, MerchantConnectorDetails, MerchantConnectorDetailsWrap, - MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, MerchantId, - PaymentMethodsEnabled, PayoutRoutingAlgorithm, PayoutStraightThroughAlgorithm, ToggleKVRequest, - ToggleKVResponse, WebhookDetails, + BusinessProfileCreate, BusinessProfileResponse, BusinessProfileUpdate, MerchantAccountCreate, + MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate, + MerchantConnectorCreate, MerchantConnectorDeleteResponse, MerchantConnectorDetails, + MerchantConnectorDetailsWrap, MerchantConnectorId, MerchantConnectorResponse, MerchantDetails, + MerchantId, PaymentMethodsEnabled, PayoutRoutingAlgorithm, ToggleKVRequest, ToggleKVResponse, + WebhookDetails, }; use common_utils::ext_traits::{Encode, ValueExt}; use error_stack::ResultExt; diff --git a/crates/router/src/types/storage.rs b/crates/router/src/types/storage.rs index 01decc89c4c..a6becf4fb62 100644 --- a/crates/router/src/types/storage.rs +++ b/crates/router/src/types/storage.rs @@ -48,7 +48,7 @@ pub use scheduler::db::process_tracker; pub use self::{ address::*, api_keys::*, authorization::*, blocklist::*, blocklist_fingerprint::*, - blocklist_lookup::*, capture::*, cards_info::*, configs::*, customers::*, + blocklist_lookup::*, business_profile::*, capture::*, cards_info::*, configs::*, customers::*, dashboard_metadata::*, dispute::*, ephemeral_key::*, events::*, file::*, fraud_check::*, gsm::*, locker_mock_up::*, mandate::*, merchant_account::*, merchant_connector_account::*, merchant_key_store::*, payment_link::*, payment_method::*, payout_attempt::*, payouts::*, diff --git a/crates/router/src/types/storage/payout_attempt.rs b/crates/router/src/types/storage/payout_attempt.rs index 966ec24bab1..6185a2b4d6d 100644 --- a/crates/router/src/types/storage/payout_attempt.rs +++ b/crates/router/src/types/storage/payout_attempt.rs @@ -1,9 +1,3 @@ pub use diesel_models::payout_attempt::{ PayoutAttempt, PayoutAttemptNew, PayoutAttemptUpdate, PayoutAttemptUpdateInternal, }; - -#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] -pub struct PayoutRoutingData { - pub routed_through: Option, - pub algorithm: Option, -} diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index f9ba7f8e923..3cd7723a269 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -861,6 +861,16 @@ impl ForeignFrom for api_models::payments::CaptureResponse { } } +impl ForeignFrom for api_enums::PaymentMethodType { + fn foreign_from(value: api_models::payouts::PayoutMethodData) -> Self { + match value { + api_models::payouts::PayoutMethodData::Bank(bank) => Self::foreign_from(bank), + api_models::payouts::PayoutMethodData::Card(_) => Self::Debit, + api_models::payouts::PayoutMethodData::Wallet(wallet) => Self::foreign_from(wallet), + } + } +} + impl ForeignFrom for api_enums::PaymentMethodType { fn foreign_from(value: api_models::payouts::Bank) -> Self { match value { diff --git a/crates/router/tests/connectors/adyen.rs b/crates/router/tests/connectors/adyen.rs index 76e2a3052af..b95b76aa8c2 100644 --- a/crates/router/tests/connectors/adyen.rs +++ b/crates/router/tests/connectors/adyen.rs @@ -24,12 +24,13 @@ impl utils::Connector for AdyenTest { } #[cfg(feature = "payouts")] - fn get_payout_data(&self) -> Option { + fn get_payout_data(&self) -> Option { use router::connector::Adyen; - Some(types::api::PayoutConnectorData { + Some(types::api::ConnectorData { connector: Box::new(&Adyen), - connector_name: types::PayoutConnectors::Adyen, + connector_name: types::Connector::Adyen, get_token: types::api::GetToken::Connector, + merchant_connector_id: None, }) } diff --git a/crates/router/tests/connectors/utils.rs b/crates/router/tests/connectors/utils.rs index bcae35d9fc3..8f749a0b2ff 100644 --- a/crates/router/tests/connectors/utils.rs +++ b/crates/router/tests/connectors/utils.rs @@ -34,7 +34,7 @@ pub trait Connector { } #[cfg(feature = "payouts")] - fn get_payout_data(&self) -> Option { + fn get_payout_data(&self) -> Option { None } } diff --git a/crates/router/tests/connectors/wise.rs b/crates/router/tests/connectors/wise.rs index de303523040..f7c01770cec 100644 --- a/crates/router/tests/connectors/wise.rs +++ b/crates/router/tests/connectors/wise.rs @@ -27,12 +27,13 @@ impl utils::Connector for WiseTest { } #[cfg(feature = "payouts")] - fn get_payout_data(&self) -> Option { + fn get_payout_data(&self) -> Option { use router::connector::Wise; - Some(types::api::PayoutConnectorData { + Some(types::api::ConnectorData { connector: Box::new(&Wise), - connector_name: types::PayoutConnectors::Wise, + connector_name: types::Connector::Wise, get_token: types::api::GetToken::Connector, + merchant_connector_id: None, }) } diff --git a/migrations/2024-01-29-100008_routing_info_for_payout_attempts/down.sql b/migrations/2024-01-29-100008_routing_info_for_payout_attempts/down.sql new file mode 100644 index 00000000000..895a4b15095 --- /dev/null +++ b/migrations/2024-01-29-100008_routing_info_for_payout_attempts/down.sql @@ -0,0 +1,14 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE payout_attempt +ALTER COLUMN connector TYPE JSONB USING jsonb_build_object ( + 'routed_through', connector, 'algorithm', routing_info +); + +ALTER TABLE payout_attempt DROP COLUMN routing_info; + +ALTER TABLE payout_attempt ALTER COLUMN connector SET NOT NULL; + + +ALTER TABLE routing_algorithm DROP COLUMN algorithm_for; + +DROP type "TransactionType"; \ No newline at end of file diff --git a/migrations/2024-01-29-100008_routing_info_for_payout_attempts/up.sql b/migrations/2024-01-29-100008_routing_info_for_payout_attempts/up.sql new file mode 100644 index 00000000000..03e9631072d --- /dev/null +++ b/migrations/2024-01-29-100008_routing_info_for_payout_attempts/up.sql @@ -0,0 +1,27 @@ +-- Your SQL goes here +ALTER TABLE payout_attempt +ALTER COLUMN connector TYPE JSONB USING jsonb_build_object ( + 'routed_through', connector, 'algorithm', NULL +); + +ALTER TABLE payout_attempt ADD COLUMN routing_info JSONB; + +UPDATE payout_attempt +SET + routing_info = connector -> 'algorithm' +WHERE + connector ->> 'algorithm' IS NOT NULL; + +ALTER TABLE payout_attempt +ALTER COLUMN connector TYPE VARCHAR(64) USING connector ->> 'routed_through'; + +ALTER TABLE payout_attempt ALTER COLUMN connector DROP NOT NULL; + +CREATE type "TransactionType" as ENUM('payment', 'payout'); + +ALTER TABLE routing_algorithm +ADD COLUMN algorithm_for "TransactionType" DEFAULT 'payment' NOT NULL; + +ALTER TABLE routing_algorithm +ALTER COLUMN algorithm_for +DROP DEFAULT; \ No newline at end of file diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index 9e64f3e9d03..61c31c07963 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -10312,7 +10312,8 @@ "description", "algorithm", "created_at", - "modified_at" + "modified_at", + "algorithm_for" ], "properties": { "id": { @@ -10337,6 +10338,9 @@ "modified_at": { "type": "integer", "format": "int64" + }, + "algorithm_for": { + "$ref": "#/components/schemas/TransactionType" } } }, @@ -15703,6 +15707,14 @@ "modified_at": { "type": "integer", "format": "int64" + }, + "algorithm_for": { + "allOf": [ + { + "$ref": "#/components/schemas/TransactionType" + } + ], + "nullable": true } } }, @@ -16231,6 +16243,12 @@ "TouchNGoRedirection": { "type": "object" }, + "TransactionType": { + "type": "string", + "enum": [ + "payment" + ] + }, "UpdateApiKeyRequest": { "type": "object", "description": "The request body for updating an API Key.",