Skip to content

Commit a51c9f0

Browse files
Chethan-raoSarthak SoniSarthak1799
authored
feat(dynamic_routing): add open router integration for success based routing (#7795)
Co-authored-by: Sarthak Soni <[email protected]> Co-authored-by: Sarthak Soni <[email protected]>
1 parent eabef32 commit a51c9f0

File tree

12 files changed

+477
-43
lines changed

12 files changed

+477
-43
lines changed

config/config.example.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -986,4 +986,8 @@ primary_color = "#006DF9"
986986
background_color = "#FFFFFF" # Email background color
987987

988988
[billing_connectors_payment_sync]
989-
billing_connectors_which_require_payment_sync = "stripebilling, recurly" # List of billing connectors which has payment sync api call
989+
billing_connectors_which_require_payment_sync = "stripebilling, recurly" # List of billing connectors which has payment sync api call
990+
991+
[open_router]
992+
enabled = true # Enable or disable Open Router
993+
url = "http://localhost:8080" # Open Router URL

config/development.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1031,3 +1031,7 @@ background_color = "#FFFFFF"
10311031

10321032
[platform]
10331033
enabled = true
1034+
1035+
[open_router]
1036+
enabled = false
1037+
url = "http://localhost:8080"

config/docker_compose.toml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -813,9 +813,9 @@ enabled = false
813813
global_tenant = { tenant_id = "global", schema = "public", redis_key_prefix = "", clickhouse_database = "default" }
814814

815815
[multitenancy.tenants.public]
816-
base_url = "http://localhost:8080"
817-
schema = "public"
818-
accounts_schema = "public"
816+
base_url = "http://localhost:8080"
817+
schema = "public"
818+
accounts_schema = "public"
819819
redis_key_prefix = ""
820820
clickhouse_database = "default"
821821

@@ -899,3 +899,6 @@ background_color = "#FFFFFF"
899899

900900
[platform]
901901
enabled = true
902+
903+
[open_router]
904+
enabled = false

crates/api_models/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub mod gsm;
2323
pub mod health_check;
2424
pub mod locker_migration;
2525
pub mod mandates;
26+
#[cfg(feature = "dynamic_routing")]
27+
pub mod open_router;
2628
pub mod organization;
2729
pub mod payment_methods;
2830
pub mod payments;

crates/api_models/src/open_router.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
use std::{collections::HashMap, fmt::Debug};
2+
3+
use common_utils::{id_type, types::MinorUnit};
4+
pub use euclid::{
5+
dssa::types::EuclidAnalysable,
6+
frontend::{
7+
ast,
8+
dir::{DirKeyKind, EuclidDirFilter},
9+
},
10+
};
11+
use serde::{Deserialize, Serialize};
12+
13+
use crate::enums::{Currency, PaymentMethod, RoutableConnectors};
14+
15+
#[derive(Debug, Clone, Serialize, Deserialize)]
16+
#[serde(rename_all = "camelCase")]
17+
pub struct OpenRouterDecideGatewayRequest {
18+
pub payment_info: PaymentInfo,
19+
pub merchant_id: id_type::ProfileId,
20+
pub eligible_gateway_list: Option<Vec<RoutableConnectors>>,
21+
pub ranking_algorithm: Option<RankingAlgorithm>,
22+
pub elimination_enabled: Option<bool>,
23+
}
24+
25+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
27+
pub enum RankingAlgorithm {
28+
SrBasedRouting,
29+
PlBasedRouting,
30+
}
31+
32+
#[derive(Debug, Clone, Serialize, Deserialize)]
33+
#[serde(rename_all = "camelCase")]
34+
pub struct PaymentInfo {
35+
pub payment_id: id_type::PaymentId,
36+
pub amount: MinorUnit,
37+
pub currency: Currency,
38+
// customerId: Option<ETCu::CustomerId>,
39+
// preferredGateway: Option<ETG::Gateway>,
40+
pub payment_type: String,
41+
// metadata: Option<String>,
42+
// internalMetadata: Option<String>,
43+
// isEmi: Option<bool>,
44+
// emiBank: Option<String>,
45+
// emiTenure: Option<i32>,
46+
pub payment_method_type: String,
47+
pub payment_method: PaymentMethod,
48+
// paymentSource: Option<String>,
49+
// authType: Option<ETCa::txn_card_info::AuthType>,
50+
// cardIssuerBankName: Option<String>,
51+
// cardIsin: Option<String>,
52+
// cardType: Option<ETCa::card_type::CardType>,
53+
// cardSwitchProvider: Option<Secret<String>>,
54+
}
55+
56+
#[derive(Debug, Serialize, Deserialize, PartialEq)]
57+
pub struct DecidedGateway {
58+
pub gateway_priority_map: Option<HashMap<String, f64>>,
59+
}
60+
61+
#[derive(Debug, Serialize, Deserialize)]
62+
pub struct ErrorResponse {
63+
pub status: String,
64+
pub error_code: String,
65+
pub error_message: String,
66+
pub priority_logic_tag: Option<String>,
67+
pub filter_wise_gateways: Option<serde_json::Value>,
68+
pub error_info: UnifiedError,
69+
pub is_dynamic_mga_enabled: bool,
70+
}
71+
72+
#[derive(Debug, Serialize, Deserialize)]
73+
pub struct UnifiedError {
74+
pub code: String,
75+
pub user_message: String,
76+
pub developer_message: String,
77+
}
78+
79+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
80+
#[serde(rename_all = "camelCase")]
81+
pub struct UpdateScorePayload {
82+
pub merchant_id: id_type::ProfileId,
83+
pub gateway: RoutableConnectors,
84+
pub status: TxnStatus,
85+
pub payment_id: id_type::PaymentId,
86+
}
87+
88+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
89+
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
90+
pub enum TxnStatus {
91+
Started,
92+
AuthenticationFailed,
93+
JuspayDeclined,
94+
PendingVBV,
95+
VBVSuccessful,
96+
Authorized,
97+
AuthorizationFailed,
98+
Charged,
99+
Authorizing,
100+
CODInitiated,
101+
Voided,
102+
VoidInitiated,
103+
Nop,
104+
CaptureInitiated,
105+
CaptureFailed,
106+
VoidFailed,
107+
AutoRefunded,
108+
PartialCharged,
109+
ToBeCharged,
110+
Pending,
111+
Failure,
112+
Declined,
113+
}
114+
115+
impl From<bool> for TxnStatus {
116+
fn from(value: bool) -> Self {
117+
match value {
118+
true => Self::Charged,
119+
_ => Self::Failure,
120+
}
121+
}
122+
}

crates/router/src/configs/secrets_transformers.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,5 +532,6 @@ pub(crate) async fn fetch_raw_secrets(
532532
network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors,
533533
theme: conf.theme,
534534
platform: conf.platform,
535+
open_router: conf.open_router,
535536
}
536537
}

crates/router/src/configs/settings.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,14 @@ pub struct Settings<S: SecretState> {
145145
pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors,
146146
pub theme: ThemeSettings,
147147
pub platform: Platform,
148+
pub open_router: OpenRouter,
148149
}
149150

151+
#[derive(Debug, Deserialize, Clone, Default)]
152+
pub struct OpenRouter {
153+
pub enabled: bool,
154+
pub url: String,
155+
}
150156
#[derive(Debug, Deserialize, Clone, Default)]
151157
pub struct Platform {
152158
pub enabled: bool,

crates/router/src/core/errors.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,10 @@ pub enum RoutingError {
407407
ContractRoutingClientInitializationError,
408408
#[error("Invalid contract based connector label received from dynamic routing service: '{0}'")]
409409
InvalidContractBasedConnectorLabel(String),
410+
#[error("Failed to perform {algo} in open_router")]
411+
OpenRouterCallFailed { algo: String },
412+
#[error("Error from open_router: {0}")]
413+
OpenRouterError(String),
410414
}
411415

412416
#[derive(Debug, Clone, thiserror::Error)]

crates/router/src/core/payments.rs

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7266,6 +7266,9 @@ where
72667266
.await
72677267
.change_context(errors::ApiErrorResponse::InternalServerError)?;
72687268

7269+
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
7270+
let payment_attempt = transaction_data.payment_attempt.clone();
7271+
72697272
let connectors = routing::perform_eligibility_analysis_with_fallback(
72707273
&state.clone(),
72717274
key_store,
@@ -7280,33 +7283,42 @@ where
72807283

72817284
// dynamic success based connector selection
72827285
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
7283-
let connectors = {
7284-
if let Some(algo) = business_profile.dynamic_routing_algorithm.clone() {
7285-
let dynamic_routing_config: api_models::routing::DynamicRoutingAlgorithmRef = algo
7286-
.parse_value("DynamicRoutingAlgorithmRef")
7287-
.change_context(errors::ApiErrorResponse::InternalServerError)
7288-
.attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?;
7289-
let dynamic_split = api_models::routing::RoutingVolumeSplit {
7290-
routing_type: api_models::routing::RoutingType::Dynamic,
7291-
split: dynamic_routing_config
7292-
.dynamic_routing_volume_split
7293-
.unwrap_or_default(),
7286+
let connectors = if let Some(algo) = business_profile.dynamic_routing_algorithm.clone() {
7287+
let dynamic_routing_config: api_models::routing::DynamicRoutingAlgorithmRef = algo
7288+
.parse_value("DynamicRoutingAlgorithmRef")
7289+
.change_context(errors::ApiErrorResponse::InternalServerError)
7290+
.attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?;
7291+
let dynamic_split = api_models::routing::RoutingVolumeSplit {
7292+
routing_type: api_models::routing::RoutingType::Dynamic,
7293+
split: dynamic_routing_config
7294+
.dynamic_routing_volume_split
7295+
.unwrap_or_default(),
7296+
};
7297+
let static_split: api_models::routing::RoutingVolumeSplit =
7298+
api_models::routing::RoutingVolumeSplit {
7299+
routing_type: api_models::routing::RoutingType::Static,
7300+
split: consts::DYNAMIC_ROUTING_MAX_VOLUME
7301+
- dynamic_routing_config
7302+
.dynamic_routing_volume_split
7303+
.unwrap_or_default(),
72947304
};
7295-
let static_split: api_models::routing::RoutingVolumeSplit =
7296-
api_models::routing::RoutingVolumeSplit {
7297-
routing_type: api_models::routing::RoutingType::Static,
7298-
split: consts::DYNAMIC_ROUTING_MAX_VOLUME
7299-
- dynamic_routing_config
7300-
.dynamic_routing_volume_split
7301-
.unwrap_or_default(),
7302-
};
7303-
let volume_split_vec = vec![dynamic_split, static_split];
7304-
let routing_choice =
7305-
routing::perform_dynamic_routing_volume_split(volume_split_vec, None)
7306-
.change_context(errors::ApiErrorResponse::InternalServerError)
7307-
.attach_printable("failed to perform volume split on routing type")?;
7305+
let volume_split_vec = vec![dynamic_split, static_split];
7306+
let routing_choice = routing::perform_dynamic_routing_volume_split(volume_split_vec, None)
7307+
.change_context(errors::ApiErrorResponse::InternalServerError)
7308+
.attach_printable("failed to perform volume split on routing type")?;
73087309

7309-
if routing_choice.routing_type.is_dynamic_routing() {
7310+
if routing_choice.routing_type.is_dynamic_routing() {
7311+
if state.conf.open_router.enabled {
7312+
routing::perform_open_routing(
7313+
state,
7314+
connectors.clone(),
7315+
business_profile,
7316+
payment_attempt,
7317+
)
7318+
.await
7319+
.map_err(|e| logger::error!(open_routing_error=?e))
7320+
.unwrap_or(connectors)
7321+
} else {
73107322
let dynamic_routing_config_params_interpolator =
73117323
routing_helpers::DynamicRoutingConfigParamsInterpolator::new(
73127324
payment_data.get_payment_attempt().payment_method,
@@ -7348,12 +7360,12 @@ where
73487360
.await
73497361
.map_err(|e| logger::error!(dynamic_routing_error=?e))
73507362
.unwrap_or(connectors)
7351-
} else {
7352-
connectors
73537363
}
73547364
} else {
73557365
connectors
73567366
}
7367+
} else {
7368+
connectors
73577369
};
73587370

73597371
let connector_data = connectors

0 commit comments

Comments
 (0)