mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 20:23:43 +08:00
feat(euclid): add dynamic routing in core flows (#6333)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -3952,12 +3952,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "status",
|
"name": "enable",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "Boolean value for mentioning the expected state of dynamic routing",
|
"description": "Feature to enable for success based routing",
|
||||||
"required": true,
|
"required": true,
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "boolean"
|
"$ref": "#/components/schemas/SuccessBasedRoutingFeatures"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
@ -3998,87 +3998,6 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/config/:algorithm_id": {
|
|
||||||
"patch": {
|
|
||||||
"tags": [
|
|
||||||
"Routing"
|
|
||||||
],
|
|
||||||
"summary": "Routing - Update config for success based dynamic routing",
|
|
||||||
"description": "Update config for success based dynamic routing",
|
|
||||||
"operationId": "Update configs for success based dynamic routing algorithm",
|
|
||||||
"parameters": [
|
|
||||||
{
|
|
||||||
"name": "account_id",
|
|
||||||
"in": "path",
|
|
||||||
"description": "Merchant id",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "profile_id",
|
|
||||||
"in": "path",
|
|
||||||
"description": "The unique identifier for a profile",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "algorithm_id",
|
|
||||||
"in": "path",
|
|
||||||
"description": "The unique identifier for routing algorithm",
|
|
||||||
"required": true,
|
|
||||||
"schema": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"requestBody": {
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/SuccessBasedRoutingConfig"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"responses": {
|
|
||||||
"200": {
|
|
||||||
"description": "Routing Algorithm updated",
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"$ref": "#/components/schemas/RoutingDictionaryRecord"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Request body is malformed"
|
|
||||||
},
|
|
||||||
"403": {
|
|
||||||
"description": "Forbidden"
|
|
||||||
},
|
|
||||||
"404": {
|
|
||||||
"description": "Resource missing"
|
|
||||||
},
|
|
||||||
"422": {
|
|
||||||
"description": "Unprocessable request"
|
|
||||||
},
|
|
||||||
"500": {
|
|
||||||
"description": "Internal server error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"security": [
|
|
||||||
{
|
|
||||||
"admin_api_key": []
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"/blocklist": {
|
"/blocklist": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"tags": [
|
"tags": [
|
||||||
@ -9458,23 +9377,6 @@
|
|||||||
"ZWL"
|
"ZWL"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"CurrentBlockThreshold": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"duration_in_mins": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"nullable": true,
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"max_total_count": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int64",
|
|
||||||
"nullable": true,
|
|
||||||
"minimum": 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"CustomerAcceptance": {
|
"CustomerAcceptance": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "This \"CustomerAcceptance\" object is passed during Payments-Confirm request, it enlists the type, time, and mode of acceptance properties related to an acceptance done by the customer. The customer_acceptance sub object is usually passed by the SDK or client.",
|
"description": "This \"CustomerAcceptance\" object is passed during Payments-Confirm request, it enlists the type, time, and mode of acceptance properties related to an acceptance done by the customer. The customer_acceptance sub object is usually passed by the SDK or client.",
|
||||||
@ -23624,80 +23526,14 @@
|
|||||||
"destination"
|
"destination"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"SuccessBasedRoutingConfig": {
|
"SuccessBasedRoutingFeatures": {
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"params": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/components/schemas/SuccessBasedRoutingConfigParams"
|
|
||||||
},
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"config": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/SuccessBasedRoutingConfigBody"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SuccessBasedRoutingConfigBody": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"min_aggregates_size": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"nullable": true,
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"default_success_rate": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "double",
|
|
||||||
"nullable": true
|
|
||||||
},
|
|
||||||
"max_aggregates_size": {
|
|
||||||
"type": "integer",
|
|
||||||
"format": "int32",
|
|
||||||
"nullable": true,
|
|
||||||
"minimum": 0
|
|
||||||
},
|
|
||||||
"current_block_threshold": {
|
|
||||||
"allOf": [
|
|
||||||
{
|
|
||||||
"$ref": "#/components/schemas/CurrentBlockThreshold"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"nullable": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SuccessBasedRoutingConfigParams": {
|
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
"PaymentMethod",
|
"metrics",
|
||||||
"PaymentMethodType",
|
"dynamic_connector_selection",
|
||||||
"Currency",
|
"none"
|
||||||
"AuthenticationType"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"SuccessBasedRoutingUpdateConfigQuery": {
|
|
||||||
"type": "object",
|
|
||||||
"required": [
|
|
||||||
"algorithm_id",
|
|
||||||
"profile_id"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"algorithm_id": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"profile_id": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SurchargeDetailsResponse": {
|
"SurchargeDetailsResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@ -23961,11 +23797,11 @@
|
|||||||
"ToggleSuccessBasedRoutingQuery": {
|
"ToggleSuccessBasedRoutingQuery": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
"status"
|
"enable"
|
||||||
],
|
],
|
||||||
"properties": {
|
"properties": {
|
||||||
"status": {
|
"enable": {
|
||||||
"type": "boolean"
|
"$ref": "#/components/schemas/SuccessBasedRoutingFeatures"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -522,22 +522,51 @@ pub struct DynamicAlgorithmWithTimestamp<T> {
|
|||||||
|
|
||||||
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct DynamicRoutingAlgorithmRef {
|
pub struct DynamicRoutingAlgorithmRef {
|
||||||
pub success_based_algorithm:
|
pub success_based_algorithm: Option<SuccessBasedAlgorithm>,
|
||||||
Option<DynamicAlgorithmWithTimestamp<common_utils::id_type::RoutingId>>,
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SuccessBasedAlgorithm {
|
||||||
|
pub algorithm_id_with_timestamp:
|
||||||
|
DynamicAlgorithmWithTimestamp<common_utils::id_type::RoutingId>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub enabled_feature: SuccessBasedRoutingFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SuccessBasedAlgorithm {
|
||||||
|
pub fn update_enabled_features(&mut self, feature_to_enable: SuccessBasedRoutingFeatures) {
|
||||||
|
self.enabled_feature = feature_to_enable
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynamicRoutingAlgorithmRef {
|
impl DynamicRoutingAlgorithmRef {
|
||||||
pub fn update_algorithm_id(&mut self, new_id: common_utils::id_type::RoutingId) {
|
pub fn update_algorithm_id(
|
||||||
self.success_based_algorithm = Some(DynamicAlgorithmWithTimestamp {
|
&mut self,
|
||||||
algorithm_id: Some(new_id),
|
new_id: common_utils::id_type::RoutingId,
|
||||||
timestamp: common_utils::date_time::now_unix_timestamp(),
|
enabled_feature: SuccessBasedRoutingFeatures,
|
||||||
|
) {
|
||||||
|
self.success_based_algorithm = Some(SuccessBasedAlgorithm {
|
||||||
|
algorithm_id_with_timestamp: DynamicAlgorithmWithTimestamp {
|
||||||
|
algorithm_id: Some(new_id),
|
||||||
|
timestamp: common_utils::date_time::now_unix_timestamp(),
|
||||||
|
},
|
||||||
|
enabled_feature,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
pub struct ToggleSuccessBasedRoutingQuery {
|
pub struct ToggleSuccessBasedRoutingQuery {
|
||||||
pub status: bool,
|
pub enable: SuccessBasedRoutingFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq, ToSchema)]
|
||||||
|
#[serde(rename_all = "snake_case")]
|
||||||
|
pub enum SuccessBasedRoutingFeatures {
|
||||||
|
Metrics,
|
||||||
|
DynamicConnectorSelection,
|
||||||
|
#[default]
|
||||||
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||||
@ -551,7 +580,7 @@ pub struct SuccessBasedRoutingUpdateConfigQuery {
|
|||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
|
||||||
pub struct ToggleSuccessBasedRoutingWrapper {
|
pub struct ToggleSuccessBasedRoutingWrapper {
|
||||||
pub profile_id: common_utils::id_type::ProfileId,
|
pub profile_id: common_utils::id_type::ProfileId,
|
||||||
pub status: bool,
|
pub feature_to_enable: SuccessBasedRoutingFeatures,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
|
||||||
|
|||||||
@ -160,7 +160,6 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
routes::routing::routing_retrieve_default_config_for_profiles,
|
routes::routing::routing_retrieve_default_config_for_profiles,
|
||||||
routes::routing::routing_update_default_config_for_profile,
|
routes::routing::routing_update_default_config_for_profile,
|
||||||
routes::routing::toggle_success_based_routing,
|
routes::routing::toggle_success_based_routing,
|
||||||
routes::routing::success_based_routing_update_configs,
|
|
||||||
|
|
||||||
// Routes for blocklist
|
// Routes for blocklist
|
||||||
routes::blocklist::remove_entry_from_blocklist,
|
routes::blocklist::remove_entry_from_blocklist,
|
||||||
@ -566,6 +565,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
api_models::routing::RoutingDictionaryRecord,
|
api_models::routing::RoutingDictionaryRecord,
|
||||||
api_models::routing::RoutingKind,
|
api_models::routing::RoutingKind,
|
||||||
api_models::routing::RoutableConnectorChoice,
|
api_models::routing::RoutableConnectorChoice,
|
||||||
|
api_models::routing::SuccessBasedRoutingFeatures,
|
||||||
api_models::routing::LinkedRoutingConfigRetrieveResponse,
|
api_models::routing::LinkedRoutingConfigRetrieveResponse,
|
||||||
api_models::routing::RoutingRetrieveResponse,
|
api_models::routing::RoutingRetrieveResponse,
|
||||||
api_models::routing::ProfileDefaultRoutingConfig,
|
api_models::routing::ProfileDefaultRoutingConfig,
|
||||||
@ -577,11 +577,6 @@ Never share your secret api keys. Keep them guarded and secure.
|
|||||||
api_models::routing::ConnectorVolumeSplit,
|
api_models::routing::ConnectorVolumeSplit,
|
||||||
api_models::routing::ConnectorSelection,
|
api_models::routing::ConnectorSelection,
|
||||||
api_models::routing::ToggleSuccessBasedRoutingQuery,
|
api_models::routing::ToggleSuccessBasedRoutingQuery,
|
||||||
api_models::routing::SuccessBasedRoutingConfig,
|
|
||||||
api_models::routing::SuccessBasedRoutingConfigParams,
|
|
||||||
api_models::routing::SuccessBasedRoutingConfigBody,
|
|
||||||
api_models::routing::CurrentBlockThreshold,
|
|
||||||
api_models::routing::SuccessBasedRoutingUpdateConfigQuery,
|
|
||||||
api_models::routing::ToggleSuccessBasedRoutingPath,
|
api_models::routing::ToggleSuccessBasedRoutingPath,
|
||||||
api_models::routing::ast::RoutableChoiceKind,
|
api_models::routing::ast::RoutableChoiceKind,
|
||||||
api_models::enums::RoutableConnectors,
|
api_models::enums::RoutableConnectors,
|
||||||
|
|||||||
@ -266,7 +266,7 @@ pub async fn routing_update_default_config_for_profile() {}
|
|||||||
params(
|
params(
|
||||||
("account_id" = String, Path, description = "Merchant id"),
|
("account_id" = String, Path, description = "Merchant id"),
|
||||||
("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"),
|
("profile_id" = String, Path, description = "Profile id under which Dynamic routing needs to be toggled"),
|
||||||
("status" = bool, Query, description = "Boolean value for mentioning the expected state of dynamic routing"),
|
("enable" = SuccessBasedRoutingFeatures, Query, description = "Feature to enable for success based routing"),
|
||||||
),
|
),
|
||||||
responses(
|
responses(
|
||||||
(status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord),
|
(status = 200, description = "Routing Algorithm created", body = RoutingDictionaryRecord),
|
||||||
@ -281,30 +281,3 @@ pub async fn routing_update_default_config_for_profile() {}
|
|||||||
security(("api_key" = []), ("jwt_key" = []))
|
security(("api_key" = []), ("jwt_key" = []))
|
||||||
)]
|
)]
|
||||||
pub async fn toggle_success_based_routing() {}
|
pub async fn toggle_success_based_routing() {}
|
||||||
|
|
||||||
#[cfg(feature = "v1")]
|
|
||||||
/// Routing - Update config for success based dynamic routing
|
|
||||||
///
|
|
||||||
/// Update config for success based dynamic routing
|
|
||||||
#[utoipa::path(
|
|
||||||
patch,
|
|
||||||
path = "/account/:account_id/business_profile/:profile_id/dynamic_routing/success_based/config/:algorithm_id",
|
|
||||||
request_body = SuccessBasedRoutingConfig,
|
|
||||||
params(
|
|
||||||
("account_id" = String, Path, description = "Merchant id"),
|
|
||||||
("profile_id" = String, Path, description = "The unique identifier for a profile"),
|
|
||||||
("algorithm_id" = String, Path, description = "The unique identifier for routing algorithm"),
|
|
||||||
),
|
|
||||||
responses(
|
|
||||||
(status = 200, description = "Routing Algorithm updated", body = RoutingDictionaryRecord),
|
|
||||||
(status = 400, description = "Request body is malformed"),
|
|
||||||
(status = 500, description = "Internal server error"),
|
|
||||||
(status = 404, description = "Resource missing"),
|
|
||||||
(status = 422, description = "Unprocessable request"),
|
|
||||||
(status = 403, description = "Forbidden"),
|
|
||||||
),
|
|
||||||
tag = "Routing",
|
|
||||||
operation_id = "Update configs for success based dynamic routing algorithm",
|
|
||||||
security(("admin_api_key" = []))
|
|
||||||
)]
|
|
||||||
pub async fn success_based_routing_update_configs() {}
|
|
||||||
|
|||||||
@ -328,6 +328,20 @@ pub enum RoutingError {
|
|||||||
VolumeSplitFailed,
|
VolumeSplitFailed,
|
||||||
#[error("Unable to parse metadata")]
|
#[error("Unable to parse metadata")]
|
||||||
MetadataParsingError,
|
MetadataParsingError,
|
||||||
|
#[error("Unable to retrieve success based routing config")]
|
||||||
|
SuccessBasedRoutingConfigError,
|
||||||
|
#[error("Unable to calculate success based routing config from dynamic routing service")]
|
||||||
|
SuccessRateCalculationError,
|
||||||
|
#[error("Success rate client from dynamic routing gRPC service not initialized")]
|
||||||
|
SuccessRateClientInitializationError,
|
||||||
|
#[error("Unable to convert from '{from}' to '{to}'")]
|
||||||
|
GenericConversionError { from: String, to: String },
|
||||||
|
#[error("Invalid success based connector label received from dynamic routing service: '{0}'")]
|
||||||
|
InvalidSuccessBasedConnectorLabel(String),
|
||||||
|
#[error("unable to find '{field}'")]
|
||||||
|
GenericNotFoundError { field: String },
|
||||||
|
#[error("Unable to deserialize from '{from}' to '{to}'")]
|
||||||
|
DeserializationError { from: String, to: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, thiserror::Error)]
|
#[derive(Debug, Clone, thiserror::Error)]
|
||||||
|
|||||||
@ -5583,6 +5583,19 @@ where
|
|||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("failed eligibility analysis and fallback")?;
|
.attach_printable("failed eligibility analysis and fallback")?;
|
||||||
|
|
||||||
|
// dynamic success based connector selection
|
||||||
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
|
let connectors = {
|
||||||
|
if business_profile.dynamic_routing_algorithm.is_some() {
|
||||||
|
routing::perform_success_based_routing(state, connectors.clone(), business_profile)
|
||||||
|
.await
|
||||||
|
.map_err(|e| logger::error!(success_rate_routing_error=?e))
|
||||||
|
.unwrap_or(connectors)
|
||||||
|
} else {
|
||||||
|
connectors
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let connector_data = connectors
|
let connector_data = connectors
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|conn| {
|
.map(|conn| {
|
||||||
|
|||||||
@ -1918,8 +1918,7 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
|||||||
|
|
||||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
{
|
{
|
||||||
if let Some(dynamic_routing_algorithm) = business_profile.dynamic_routing_algorithm.clone()
|
if business_profile.dynamic_routing_algorithm.is_some() {
|
||||||
{
|
|
||||||
let state = state.clone();
|
let state = state.clone();
|
||||||
let business_profile = business_profile.clone();
|
let business_profile = business_profile.clone();
|
||||||
let payment_attempt = payment_attempt.clone();
|
let payment_attempt = payment_attempt.clone();
|
||||||
@ -1930,7 +1929,6 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
|
|||||||
&payment_attempt,
|
&payment_attempt,
|
||||||
routable_connectors,
|
routable_connectors,
|
||||||
&business_profile,
|
&business_profile,
|
||||||
dynamic_routing_algorithm,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| logger::error!(dynamic_routing_metrics_error=?e))
|
.map_err(|e| logger::error!(dynamic_routing_metrics_error=?e))
|
||||||
|
|||||||
@ -7,6 +7,8 @@ use std::{
|
|||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
|
use api_models::routing as api_routing;
|
||||||
use api_models::{
|
use api_models::{
|
||||||
admin as admin_api,
|
admin as admin_api,
|
||||||
enums::{self as api_enums, CountryAlpha2},
|
enums::{self as api_enums, CountryAlpha2},
|
||||||
@ -21,6 +23,10 @@ use euclid::{
|
|||||||
enums as euclid_enums,
|
enums as euclid_enums,
|
||||||
frontend::{ast, dir as euclid_dir},
|
frontend::{ast, dir as euclid_dir},
|
||||||
};
|
};
|
||||||
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
|
use external_services::grpc_client::dynamic_routing::{
|
||||||
|
success_rate::CalSuccessRateResponse, SuccessBasedDynamicRouting,
|
||||||
|
};
|
||||||
use kgraph_utils::{
|
use kgraph_utils::{
|
||||||
mca as mca_graph,
|
mca as mca_graph,
|
||||||
transformers::{IntoContext, IntoDirValue},
|
transformers::{IntoContext, IntoDirValue},
|
||||||
@ -1227,3 +1233,114 @@ pub fn make_dsl_input_for_surcharge(
|
|||||||
};
|
};
|
||||||
Ok(backend_input)
|
Ok(backend_input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// success based dynamic routing
|
||||||
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
|
pub async fn perform_success_based_routing(
|
||||||
|
state: &SessionState,
|
||||||
|
routable_connectors: Vec<api_routing::RoutableConnectorChoice>,
|
||||||
|
business_profile: &domain::Profile,
|
||||||
|
) -> RoutingResult<Vec<api_routing::RoutableConnectorChoice>> {
|
||||||
|
let success_based_dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef =
|
||||||
|
business_profile
|
||||||
|
.dynamic_routing_algorithm
|
||||||
|
.clone()
|
||||||
|
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
|
||||||
|
.transpose()
|
||||||
|
.change_context(errors::RoutingError::DeserializationError {
|
||||||
|
from: "JSON".to_string(),
|
||||||
|
to: "DynamicRoutingAlgorithmRef".to_string(),
|
||||||
|
})
|
||||||
|
.attach_printable("unable to deserialize DynamicRoutingAlgorithmRef from JSON")?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
let success_based_algo_ref = success_based_dynamic_routing_algo_ref
|
||||||
|
.success_based_algorithm
|
||||||
|
.ok_or(errors::RoutingError::GenericNotFoundError { field: "success_based_algorithm".to_string() })
|
||||||
|
.attach_printable(
|
||||||
|
"success_based_algorithm not found in dynamic_routing_algorithm from business_profile table",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if success_based_algo_ref.enabled_feature
|
||||||
|
== api_routing::SuccessBasedRoutingFeatures::DynamicConnectorSelection
|
||||||
|
{
|
||||||
|
logger::debug!(
|
||||||
|
"performing success_based_routing for profile {}",
|
||||||
|
business_profile.get_id().get_string_repr()
|
||||||
|
);
|
||||||
|
let client = state
|
||||||
|
.grpc_client
|
||||||
|
.dynamic_routing
|
||||||
|
.success_rate_client
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(errors::RoutingError::SuccessRateClientInitializationError)
|
||||||
|
.attach_printable("success_rate gRPC client not found")?;
|
||||||
|
|
||||||
|
let success_based_routing_configs = routing::helpers::fetch_success_based_routing_configs(
|
||||||
|
state,
|
||||||
|
business_profile,
|
||||||
|
success_based_algo_ref
|
||||||
|
.algorithm_id_with_timestamp
|
||||||
|
.algorithm_id
|
||||||
|
.ok_or(errors::RoutingError::GenericNotFoundError {
|
||||||
|
field: "success_based_routing_algorithm_id".to_string(),
|
||||||
|
})
|
||||||
|
.attach_printable(
|
||||||
|
"success_based_routing_algorithm_id not found in business_profile",
|
||||||
|
)?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::RoutingError::SuccessBasedRoutingConfigError)
|
||||||
|
.attach_printable("unable to fetch success_rate based dynamic routing configs")?;
|
||||||
|
|
||||||
|
let tenant_business_profile_id = routing::helpers::generate_tenant_business_profile_id(
|
||||||
|
&state.tenant.redis_key_prefix,
|
||||||
|
business_profile.get_id().get_string_repr(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let success_based_connectors: CalSuccessRateResponse = client
|
||||||
|
.calculate_success_rate(
|
||||||
|
tenant_business_profile_id,
|
||||||
|
success_based_routing_configs,
|
||||||
|
routable_connectors,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::RoutingError::SuccessRateCalculationError)
|
||||||
|
.attach_printable(
|
||||||
|
"unable to calculate/fetch success rate from dynamic routing service",
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let mut connectors = Vec::with_capacity(success_based_connectors.labels_with_score.len());
|
||||||
|
for label_with_score in success_based_connectors.labels_with_score {
|
||||||
|
let (connector, merchant_connector_id) = label_with_score.label
|
||||||
|
.split_once(':')
|
||||||
|
.ok_or(errors::RoutingError::InvalidSuccessBasedConnectorLabel(label_with_score.label.to_string()))
|
||||||
|
.attach_printable(
|
||||||
|
"unable to split connector_name and mca_id from the label obtained by the dynamic routing service",
|
||||||
|
)?;
|
||||||
|
connectors.push(api_routing::RoutableConnectorChoice {
|
||||||
|
choice_kind: api_routing::RoutableChoiceKind::FullStruct,
|
||||||
|
connector: common_enums::RoutableConnectors::from_str(connector)
|
||||||
|
.change_context(errors::RoutingError::GenericConversionError {
|
||||||
|
from: "String".to_string(),
|
||||||
|
to: "RoutableConnectors".to_string(),
|
||||||
|
})
|
||||||
|
.attach_printable("unable to convert String to RoutableConnectors")?,
|
||||||
|
merchant_connector_id: Some(
|
||||||
|
common_utils::id_type::MerchantConnectorAccountId::wrap(
|
||||||
|
merchant_connector_id.to_string(),
|
||||||
|
)
|
||||||
|
.change_context(errors::RoutingError::GenericConversionError {
|
||||||
|
from: "String".to_string(),
|
||||||
|
to: "MerchantConnectorAccountId".to_string(),
|
||||||
|
})
|
||||||
|
.attach_printable("unable to convert MerchantConnectorAccountId from string")?,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
logger::debug!(success_based_routing_connectors=?connectors);
|
||||||
|
Ok(connectors)
|
||||||
|
} else {
|
||||||
|
Ok(routable_connectors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -441,9 +441,13 @@ pub async fn link_routing_config(
|
|||||||
utils::when(
|
utils::when(
|
||||||
matches!(
|
matches!(
|
||||||
dynamic_routing_ref.success_based_algorithm,
|
dynamic_routing_ref.success_based_algorithm,
|
||||||
Some(routing_types::DynamicAlgorithmWithTimestamp {
|
Some(routing::SuccessBasedAlgorithm {
|
||||||
algorithm_id: Some(ref id),
|
algorithm_id_with_timestamp:
|
||||||
timestamp: _
|
routing_types::DynamicAlgorithmWithTimestamp {
|
||||||
|
algorithm_id: Some(ref id),
|
||||||
|
timestamp: _
|
||||||
|
},
|
||||||
|
enabled_feature: _
|
||||||
}) if id == &algorithm_id
|
}) if id == &algorithm_id
|
||||||
),
|
),
|
||||||
|| {
|
|| {
|
||||||
@ -453,7 +457,17 @@ pub async fn link_routing_config(
|
|||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
dynamic_routing_ref.update_algorithm_id(algorithm_id);
|
dynamic_routing_ref.update_algorithm_id(
|
||||||
|
algorithm_id,
|
||||||
|
dynamic_routing_ref
|
||||||
|
.success_based_algorithm
|
||||||
|
.clone()
|
||||||
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable(
|
||||||
|
"missing success_based_algorithm in dynamic_algorithm_ref from business_profile table",
|
||||||
|
)?
|
||||||
|
.enabled_feature,
|
||||||
|
);
|
||||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||||
db,
|
db,
|
||||||
key_manager_state,
|
key_manager_state,
|
||||||
@ -1169,7 +1183,7 @@ pub async fn toggle_success_based_routing(
|
|||||||
state: SessionState,
|
state: SessionState,
|
||||||
merchant_account: domain::MerchantAccount,
|
merchant_account: domain::MerchantAccount,
|
||||||
key_store: domain::MerchantKeyStore,
|
key_store: domain::MerchantKeyStore,
|
||||||
status: bool,
|
feature_to_enable: routing::SuccessBasedRoutingFeatures,
|
||||||
profile_id: common_utils::id_type::ProfileId,
|
profile_id: common_utils::id_type::ProfileId,
|
||||||
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
|
) -> RouterResponse<routing_types::RoutingDictionaryRecord> {
|
||||||
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(
|
metrics::ROUTING_CREATE_REQUEST_RECEIVED.add(
|
||||||
@ -1205,115 +1219,158 @@ pub async fn toggle_success_based_routing(
|
|||||||
)?
|
)?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
if status {
|
match feature_to_enable {
|
||||||
let default_success_based_routing_config = routing::SuccessBasedRoutingConfig::default();
|
routing::SuccessBasedRoutingFeatures::Metrics
|
||||||
let algorithm_id = common_utils::generate_routing_id_of_default_length();
|
| routing::SuccessBasedRoutingFeatures::DynamicConnectorSelection => {
|
||||||
let timestamp = common_utils::date_time::now();
|
if let Some(ref mut algo_with_timestamp) =
|
||||||
let algo = RoutingAlgorithm {
|
success_based_dynamic_routing_algo_ref.success_based_algorithm
|
||||||
algorithm_id: algorithm_id.clone(),
|
{
|
||||||
profile_id: business_profile.get_id().to_owned(),
|
match algo_with_timestamp
|
||||||
merchant_id: merchant_account.get_id().to_owned(),
|
.algorithm_id_with_timestamp
|
||||||
name: "Dynamic routing algorithm".to_string(),
|
.algorithm_id
|
||||||
description: None,
|
.clone()
|
||||||
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
|
{
|
||||||
algorithm_data: serde_json::json!(default_success_based_routing_config),
|
Some(algorithm_id) => {
|
||||||
created_at: timestamp,
|
// algorithm is already present in profile
|
||||||
modified_at: timestamp,
|
if algo_with_timestamp.enabled_feature == feature_to_enable {
|
||||||
algorithm_for: common_enums::TransactionType::Payment,
|
// algorithm already has the required feature
|
||||||
};
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||||
|
message: "Success rate based routing is already enabled"
|
||||||
|
.to_string(),
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
// enable the requested feature for the algorithm
|
||||||
|
algo_with_timestamp.update_enabled_features(feature_to_enable);
|
||||||
|
let record = db
|
||||||
|
.find_routing_algorithm_by_profile_id_algorithm_id(
|
||||||
|
business_profile.get_id(),
|
||||||
|
&algorithm_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(
|
||||||
|
errors::ApiErrorResponse::ResourceIdNotFound,
|
||||||
|
)?;
|
||||||
|
let response = record.foreign_into();
|
||||||
|
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||||
|
db,
|
||||||
|
key_manager_state,
|
||||||
|
&key_store,
|
||||||
|
business_profile,
|
||||||
|
success_based_dynamic_routing_algo_ref,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let record = db
|
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
||||||
.insert_routing_algorithm(algo)
|
&metrics::CONTEXT,
|
||||||
.await
|
1,
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
&add_attributes([(
|
||||||
.attach_printable("Unable to insert record in routing algorithm table")?;
|
"profile_id",
|
||||||
|
profile_id.get_string_repr().to_owned(),
|
||||||
success_based_dynamic_routing_algo_ref.update_algorithm_id(algorithm_id);
|
)]),
|
||||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
);
|
||||||
db,
|
Ok(service_api::ApplicationResponse::Json(response))
|
||||||
key_manager_state,
|
}
|
||||||
&key_store,
|
}
|
||||||
business_profile,
|
None => {
|
||||||
success_based_dynamic_routing_algo_ref,
|
// algorithm isn't present in profile
|
||||||
)
|
helpers::default_success_based_routing_setup(
|
||||||
.await?;
|
&state,
|
||||||
|
key_store,
|
||||||
let new_record = record.foreign_into();
|
business_profile,
|
||||||
|
feature_to_enable,
|
||||||
metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
merchant_account.get_id().to_owned(),
|
||||||
&metrics::CONTEXT,
|
success_based_dynamic_routing_algo_ref,
|
||||||
1,
|
|
||||||
&add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]),
|
|
||||||
);
|
|
||||||
Ok(service_api::ApplicationResponse::Json(new_record))
|
|
||||||
} else {
|
|
||||||
let timestamp = common_utils::date_time::now_unix_timestamp();
|
|
||||||
match success_based_dynamic_routing_algo_ref.success_based_algorithm {
|
|
||||||
Some(algorithm_ref) => {
|
|
||||||
if let Some(algorithm_id) = algorithm_ref.algorithm_id {
|
|
||||||
let dynamic_routing_algorithm = routing_types::DynamicRoutingAlgorithmRef {
|
|
||||||
success_based_algorithm: Some(
|
|
||||||
routing_types::DynamicAlgorithmWithTimestamp {
|
|
||||||
algorithm_id: None,
|
|
||||||
timestamp,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
// redact cache for success based routing configs
|
|
||||||
let cache_key = format!(
|
|
||||||
"{}_{}",
|
|
||||||
business_profile.get_id().get_string_repr(),
|
|
||||||
algorithm_id.get_string_repr()
|
|
||||||
);
|
|
||||||
let cache_entries_to_redact =
|
|
||||||
vec![cache::CacheKind::SuccessBasedDynamicRoutingCache(
|
|
||||||
cache_key.into(),
|
|
||||||
)];
|
|
||||||
let _ = cache::publish_into_redact_channel(
|
|
||||||
state.store.get_cache_store().as_ref(),
|
|
||||||
cache_entries_to_redact,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
logger::error!(
|
|
||||||
"unable to publish into the redact channel for evicting the success based routing config cache {e:?}"
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
let record = db
|
|
||||||
.find_routing_algorithm_by_profile_id_algorithm_id(
|
|
||||||
business_profile.get_id(),
|
|
||||||
&algorithm_id,
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
}
|
||||||
let response = record.foreign_into();
|
|
||||||
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
|
||||||
db,
|
|
||||||
key_manager_state,
|
|
||||||
&key_store,
|
|
||||||
business_profile,
|
|
||||||
dynamic_routing_algorithm,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
|
|
||||||
&metrics::CONTEXT,
|
|
||||||
1,
|
|
||||||
&add_attributes([("profile_id", profile_id.get_string_repr().to_owned())]),
|
|
||||||
);
|
|
||||||
|
|
||||||
Ok(service_api::ApplicationResponse::Json(response))
|
|
||||||
} else {
|
|
||||||
Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
||||||
message: "Algorithm is already inactive".to_string(),
|
|
||||||
})?
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// algorithm isn't present in profile
|
||||||
|
helpers::default_success_based_routing_setup(
|
||||||
|
&state,
|
||||||
|
key_store,
|
||||||
|
business_profile,
|
||||||
|
feature_to_enable,
|
||||||
|
merchant_account.get_id().to_owned(),
|
||||||
|
success_based_dynamic_routing_algo_ref,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
routing::SuccessBasedRoutingFeatures::None => {
|
||||||
|
// disable success based routing for the requested profile
|
||||||
|
let timestamp = common_utils::date_time::now_unix_timestamp();
|
||||||
|
match success_based_dynamic_routing_algo_ref.success_based_algorithm {
|
||||||
|
Some(algorithm_ref) => {
|
||||||
|
if let Some(algorithm_id) =
|
||||||
|
algorithm_ref.algorithm_id_with_timestamp.algorithm_id
|
||||||
|
{
|
||||||
|
let dynamic_routing_algorithm = routing_types::DynamicRoutingAlgorithmRef {
|
||||||
|
success_based_algorithm: Some(routing::SuccessBasedAlgorithm {
|
||||||
|
algorithm_id_with_timestamp:
|
||||||
|
routing_types::DynamicAlgorithmWithTimestamp {
|
||||||
|
algorithm_id: None,
|
||||||
|
timestamp,
|
||||||
|
},
|
||||||
|
enabled_feature: routing::SuccessBasedRoutingFeatures::None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// redact cache for success based routing configs
|
||||||
|
let cache_key = format!(
|
||||||
|
"{}_{}",
|
||||||
|
business_profile.get_id().get_string_repr(),
|
||||||
|
algorithm_id.get_string_repr()
|
||||||
|
);
|
||||||
|
let cache_entries_to_redact =
|
||||||
|
vec![cache::CacheKind::SuccessBasedDynamicRoutingCache(
|
||||||
|
cache_key.into(),
|
||||||
|
)];
|
||||||
|
let _ = cache::publish_into_redact_channel(
|
||||||
|
state.store.get_cache_store().as_ref(),
|
||||||
|
cache_entries_to_redact,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("unable to publish into the redact channel for evicting the success based routing config cache")?;
|
||||||
|
|
||||||
|
let record = db
|
||||||
|
.find_routing_algorithm_by_profile_id_algorithm_id(
|
||||||
|
business_profile.get_id(),
|
||||||
|
&algorithm_id,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.to_not_found_response(errors::ApiErrorResponse::ResourceIdNotFound)?;
|
||||||
|
let response = record.foreign_into();
|
||||||
|
helpers::update_business_profile_active_dynamic_algorithm_ref(
|
||||||
|
db,
|
||||||
|
key_manager_state,
|
||||||
|
&key_store,
|
||||||
|
business_profile,
|
||||||
|
dynamic_routing_algorithm,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
metrics::ROUTING_UNLINK_CONFIG_SUCCESS_RESPONSE.add(
|
||||||
|
&metrics::CONTEXT,
|
||||||
|
1,
|
||||||
|
&add_attributes([(
|
||||||
|
"profile_id",
|
||||||
|
profile_id.get_string_repr().to_owned(),
|
||||||
|
)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(service_api::ApplicationResponse::Json(response))
|
||||||
|
} else {
|
||||||
|
Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||||
|
message: "Algorithm is already inactive".to_string(),
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => Err(errors::ApiErrorResponse::PreconditionFailed {
|
||||||
|
message: "Success rate based routing is already disabled".to_string(),
|
||||||
|
})?,
|
||||||
}
|
}
|
||||||
None => Err(errors::ApiErrorResponse::PreconditionFailed {
|
|
||||||
message: "Algorithm is already inactive".to_string(),
|
|
||||||
})?,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,10 +12,16 @@ use api_models::routing as routing_types;
|
|||||||
use common_utils::ext_traits::ValueExt;
|
use common_utils::ext_traits::ValueExt;
|
||||||
use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState};
|
use common_utils::{ext_traits::Encode, id_type, types::keymanager::KeyManagerState};
|
||||||
use diesel_models::configs;
|
use diesel_models::configs;
|
||||||
|
#[cfg(feature = "v1")]
|
||||||
|
use diesel_models::routing_algorithm;
|
||||||
use error_stack::ResultExt;
|
use error_stack::ResultExt;
|
||||||
#[cfg(feature = "dynamic_routing")]
|
|
||||||
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
|
|
||||||
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
||||||
|
use external_services::grpc_client::dynamic_routing::SuccessBasedDynamicRouting;
|
||||||
|
#[cfg(feature = "v1")]
|
||||||
|
use hyperswitch_domain_models::api::ApplicationResponse;
|
||||||
|
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
||||||
|
use router_env::logger;
|
||||||
|
#[cfg(any(feature = "dynamic_routing", feature = "v1"))]
|
||||||
use router_env::{instrument, metrics::add_attributes, tracing};
|
use router_env::{instrument, metrics::add_attributes, tracing};
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
use storage_impl::redis::cache;
|
use storage_impl::redis::cache;
|
||||||
@ -29,8 +35,10 @@ use crate::{
|
|||||||
types::{domain, storage},
|
types::{domain, storage},
|
||||||
utils::StringExt,
|
utils::StringExt,
|
||||||
};
|
};
|
||||||
#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
|
#[cfg(feature = "v1")]
|
||||||
use crate::{core::metrics as core_metrics, routes::metrics};
|
use crate::{core::metrics as core_metrics, routes::metrics, types::transformers::ForeignInto};
|
||||||
|
pub const SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM: &str =
|
||||||
|
"Success rate based dynamic routing algorithm";
|
||||||
|
|
||||||
/// Provides us with all the configured configs of the Merchant in the ascending time configured
|
/// Provides us with all the configured configs of the Merchant in the ascending time configured
|
||||||
/// manner and chooses the first of them
|
/// manner and chooses the first of them
|
||||||
@ -594,28 +602,8 @@ pub async fn refresh_success_based_routing_cache(
|
|||||||
pub async fn fetch_success_based_routing_configs(
|
pub async fn fetch_success_based_routing_configs(
|
||||||
state: &SessionState,
|
state: &SessionState,
|
||||||
business_profile: &domain::Profile,
|
business_profile: &domain::Profile,
|
||||||
dynamic_routing_algorithm: serde_json::Value,
|
success_based_routing_id: id_type::RoutingId,
|
||||||
) -> RouterResult<routing_types::SuccessBasedRoutingConfig> {
|
) -> RouterResult<routing_types::SuccessBasedRoutingConfig> {
|
||||||
let dynamic_routing_algorithm_ref = dynamic_routing_algorithm
|
|
||||||
.parse_value::<routing_types::DynamicRoutingAlgorithmRef>("DynamicRoutingAlgorithmRef")
|
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
|
||||||
.attach_printable("unable to parse dynamic_routing_algorithm_ref")?;
|
|
||||||
|
|
||||||
let success_based_routing_id = dynamic_routing_algorithm_ref
|
|
||||||
.success_based_algorithm
|
|
||||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
|
||||||
message: "success_based_algorithm not found in dynamic_routing_algorithm_ref"
|
|
||||||
.to_string(),
|
|
||||||
})?
|
|
||||||
.algorithm_id
|
|
||||||
// error can be possible when the feature is toggled off.
|
|
||||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
|
||||||
message: format!(
|
|
||||||
"unable to find algorithm_id in success based algorithm config as the feature is disabled for profile_id: {}",
|
|
||||||
business_profile.get_id().get_string_repr()
|
|
||||||
),
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let key = format!(
|
let key = format!(
|
||||||
"{}_{}",
|
"{}_{}",
|
||||||
business_profile.get_id().get_string_repr(),
|
business_profile.get_id().get_string_repr(),
|
||||||
@ -657,156 +645,185 @@ pub async fn push_metrics_for_success_based_routing(
|
|||||||
payment_attempt: &storage::PaymentAttempt,
|
payment_attempt: &storage::PaymentAttempt,
|
||||||
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
|
routable_connectors: Vec<routing_types::RoutableConnectorChoice>,
|
||||||
business_profile: &domain::Profile,
|
business_profile: &domain::Profile,
|
||||||
dynamic_routing_algorithm: serde_json::Value,
|
|
||||||
) -> RouterResult<()> {
|
) -> RouterResult<()> {
|
||||||
let client = state
|
let success_based_dynamic_routing_algo_ref: routing_types::DynamicRoutingAlgorithmRef =
|
||||||
.grpc_client
|
business_profile
|
||||||
.dynamic_routing
|
.dynamic_routing_algorithm
|
||||||
.success_rate_client
|
.clone()
|
||||||
.as_ref()
|
.map(|val| val.parse_value("DynamicRoutingAlgorithmRef"))
|
||||||
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
.transpose()
|
||||||
message: "success_rate gRPC client not found".to_string(),
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
})?;
|
.attach_printable("Failed to deserialize DynamicRoutingAlgorithmRef from JSON")?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
let success_based_algo_ref = success_based_dynamic_routing_algo_ref
|
||||||
errors::ApiErrorResponse::GenericNotFoundError {
|
.success_based_algorithm
|
||||||
message: "unable to derive payment connector from payment attempt".to_string(),
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
},
|
.attach_printable("success_based_algorithm not found in dynamic_routing_algorithm from business_profile table")?;
|
||||||
)?;
|
|
||||||
|
|
||||||
let success_based_routing_configs =
|
if success_based_algo_ref.enabled_feature != routing_types::SuccessBasedRoutingFeatures::None {
|
||||||
fetch_success_based_routing_configs(state, business_profile, dynamic_routing_algorithm)
|
let client = state
|
||||||
|
.grpc_client
|
||||||
|
.dynamic_routing
|
||||||
|
.success_rate_client
|
||||||
|
.as_ref()
|
||||||
|
.ok_or(errors::ApiErrorResponse::GenericNotFoundError {
|
||||||
|
message: "success_rate gRPC client not found".to_string(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let payment_connector = &payment_attempt.connector.clone().ok_or(
|
||||||
|
errors::ApiErrorResponse::GenericNotFoundError {
|
||||||
|
message: "unable to derive payment connector from payment attempt".to_string(),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let success_based_routing_configs = fetch_success_based_routing_configs(
|
||||||
|
state,
|
||||||
|
business_profile,
|
||||||
|
success_based_algo_ref
|
||||||
|
.algorithm_id_with_timestamp
|
||||||
|
.algorithm_id
|
||||||
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable(
|
||||||
|
"success_based_routing_algorithm_id not found in business_profile",
|
||||||
|
)?,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
|
||||||
|
|
||||||
|
let tenant_business_profile_id = generate_tenant_business_profile_id(
|
||||||
|
&state.tenant.redis_key_prefix,
|
||||||
|
business_profile.get_id().get_string_repr(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let success_based_connectors = client
|
||||||
|
.calculate_success_rate(
|
||||||
|
tenant_business_profile_id.clone(),
|
||||||
|
success_based_routing_configs.clone(),
|
||||||
|
routable_connectors.clone(),
|
||||||
|
)
|
||||||
.await
|
.await
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("unable to retrieve success_rate based dynamic routing configs")?;
|
.attach_printable(
|
||||||
|
"unable to calculate/fetch success rate from dynamic routing service",
|
||||||
|
)?;
|
||||||
|
|
||||||
let tenant_business_profile_id = format!(
|
let payment_status_attribute =
|
||||||
"{}:{}",
|
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
|
||||||
state.tenant.redis_key_prefix,
|
|
||||||
business_profile.get_id().get_string_repr()
|
|
||||||
);
|
|
||||||
|
|
||||||
let success_based_connectors = client
|
let first_success_based_connector_label = &success_based_connectors
|
||||||
.calculate_success_rate(
|
.labels_with_score
|
||||||
tenant_business_profile_id.clone(),
|
.first()
|
||||||
success_based_routing_configs.clone(),
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
routable_connectors.clone(),
|
.attach_printable(
|
||||||
)
|
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
||||||
.await
|
)?
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.label
|
||||||
.attach_printable("unable to calculate/fetch success rate from dynamic routing service")?;
|
.to_string();
|
||||||
|
|
||||||
let payment_status_attribute =
|
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
|
||||||
get_desired_payment_status_for_success_routing_metrics(&payment_attempt.status);
|
.split_once(':')
|
||||||
|
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable(
|
||||||
|
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
|
||||||
|
)?;
|
||||||
|
|
||||||
let first_success_based_connector_label = &success_based_connectors
|
let outcome = get_success_based_metrics_outcome_for_payment(
|
||||||
.labels_with_score
|
&payment_status_attribute,
|
||||||
.first()
|
payment_connector.to_string(),
|
||||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
first_success_based_connector.to_string(),
|
||||||
.attach_printable(
|
);
|
||||||
"unable to fetch the first connector from list of connectors obtained from dynamic routing service",
|
|
||||||
)?
|
|
||||||
.label
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let (first_success_based_connector, merchant_connector_id) = first_success_based_connector_label
|
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
|
||||||
.split_once(':')
|
&metrics::CONTEXT,
|
||||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
1,
|
||||||
.attach_printable(
|
&add_attributes([
|
||||||
"unable to split connector_name and mca_id from the first connector obtained from dynamic routing service",
|
("tenant", state.tenant.name.clone()),
|
||||||
)?;
|
(
|
||||||
|
"merchant_id",
|
||||||
let outcome = get_success_based_metrics_outcome_for_payment(
|
payment_attempt.merchant_id.get_string_repr().to_string(),
|
||||||
&payment_status_attribute,
|
|
||||||
payment_connector.to_string(),
|
|
||||||
first_success_based_connector.to_string(),
|
|
||||||
);
|
|
||||||
|
|
||||||
core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add(
|
|
||||||
&metrics::CONTEXT,
|
|
||||||
1,
|
|
||||||
&add_attributes([
|
|
||||||
("tenant", state.tenant.name.clone()),
|
|
||||||
(
|
|
||||||
"merchant_id",
|
|
||||||
payment_attempt.merchant_id.get_string_repr().to_string(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"profile_id",
|
|
||||||
payment_attempt.profile_id.get_string_repr().to_string(),
|
|
||||||
),
|
|
||||||
("merchant_connector_id", merchant_connector_id.to_string()),
|
|
||||||
(
|
|
||||||
"payment_id",
|
|
||||||
payment_attempt.payment_id.get_string_repr().to_string(),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"success_based_routing_connector",
|
|
||||||
first_success_based_connector.to_string(),
|
|
||||||
),
|
|
||||||
("payment_connector", payment_connector.to_string()),
|
|
||||||
(
|
|
||||||
"currency",
|
|
||||||
payment_attempt
|
|
||||||
.currency
|
|
||||||
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"payment_method",
|
|
||||||
payment_attempt.payment_method.map_or_else(
|
|
||||||
|| "None".to_string(),
|
|
||||||
|payment_method| payment_method.to_string(),
|
|
||||||
),
|
),
|
||||||
),
|
(
|
||||||
(
|
"profile_id",
|
||||||
"payment_method_type",
|
payment_attempt.profile_id.get_string_repr().to_string(),
|
||||||
payment_attempt.payment_method_type.map_or_else(
|
|
||||||
|| "None".to_string(),
|
|
||||||
|payment_method_type| payment_method_type.to_string(),
|
|
||||||
),
|
),
|
||||||
),
|
("merchant_connector_id", merchant_connector_id.to_string()),
|
||||||
(
|
(
|
||||||
"capture_method",
|
"payment_id",
|
||||||
payment_attempt.capture_method.map_or_else(
|
payment_attempt.payment_id.get_string_repr().to_string(),
|
||||||
|| "None".to_string(),
|
|
||||||
|capture_method| capture_method.to_string(),
|
|
||||||
),
|
),
|
||||||
),
|
(
|
||||||
(
|
"success_based_routing_connector",
|
||||||
"authentication_type",
|
first_success_based_connector.to_string(),
|
||||||
payment_attempt.authentication_type.map_or_else(
|
|
||||||
|| "None".to_string(),
|
|
||||||
|authentication_type| authentication_type.to_string(),
|
|
||||||
),
|
),
|
||||||
),
|
("payment_connector", payment_connector.to_string()),
|
||||||
("payment_status", payment_attempt.status.to_string()),
|
(
|
||||||
("conclusive_classification", outcome.to_string()),
|
"currency",
|
||||||
]),
|
payment_attempt
|
||||||
);
|
.currency
|
||||||
|
.map_or_else(|| "None".to_string(), |currency| currency.to_string()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"payment_method",
|
||||||
|
payment_attempt.payment_method.map_or_else(
|
||||||
|
|| "None".to_string(),
|
||||||
|
|payment_method| payment_method.to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"payment_method_type",
|
||||||
|
payment_attempt.payment_method_type.map_or_else(
|
||||||
|
|| "None".to_string(),
|
||||||
|
|payment_method_type| payment_method_type.to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"capture_method",
|
||||||
|
payment_attempt.capture_method.map_or_else(
|
||||||
|
|| "None".to_string(),
|
||||||
|
|capture_method| capture_method.to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"authentication_type",
|
||||||
|
payment_attempt.authentication_type.map_or_else(
|
||||||
|
|| "None".to_string(),
|
||||||
|
|authentication_type| authentication_type.to_string(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("payment_status", payment_attempt.status.to_string()),
|
||||||
|
("conclusive_classification", outcome.to_string()),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
logger::debug!("successfully pushed success_based_routing metrics");
|
||||||
|
|
||||||
client
|
client
|
||||||
.update_success_rate(
|
.update_success_rate(
|
||||||
tenant_business_profile_id,
|
tenant_business_profile_id,
|
||||||
success_based_routing_configs,
|
success_based_routing_configs,
|
||||||
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
|
vec![routing_types::RoutableConnectorChoiceWithStatus::new(
|
||||||
routing_types::RoutableConnectorChoice {
|
routing_types::RoutableConnectorChoice {
|
||||||
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
|
choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
|
||||||
connector: common_enums::RoutableConnectors::from_str(
|
connector: common_enums::RoutableConnectors::from_str(
|
||||||
payment_connector.as_str(),
|
payment_connector.as_str(),
|
||||||
)
|
)
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable("unable to infer routable_connector from connector")?,
|
.attach_printable("unable to infer routable_connector from connector")?,
|
||||||
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
|
merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
|
||||||
},
|
},
|
||||||
payment_status_attribute == common_enums::AttemptStatus::Charged,
|
payment_status_attribute == common_enums::AttemptStatus::Charged,
|
||||||
)],
|
)],
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
.attach_printable(
|
.attach_printable(
|
||||||
"unable to update success based routing window in dynamic routing service",
|
"unable to update success based routing window in dynamic routing service",
|
||||||
)?;
|
)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
|
||||||
@ -875,3 +892,67 @@ fn get_success_based_metrics_outcome_for_payment(
|
|||||||
_ => common_enums::SuccessBasedRoutingConclusiveState::NonDeterministic,
|
_ => common_enums::SuccessBasedRoutingConclusiveState::NonDeterministic,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// generates cache key with tenant's redis key prefix and profile_id
|
||||||
|
pub fn generate_tenant_business_profile_id(
|
||||||
|
redis_key_prefix: &str,
|
||||||
|
business_profile_id: &str,
|
||||||
|
) -> String {
|
||||||
|
format!("{}:{}", redis_key_prefix, business_profile_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// default config setup for success_based_routing
|
||||||
|
#[cfg(feature = "v1")]
|
||||||
|
#[instrument(skip_all)]
|
||||||
|
pub async fn default_success_based_routing_setup(
|
||||||
|
state: &SessionState,
|
||||||
|
key_store: domain::MerchantKeyStore,
|
||||||
|
business_profile: domain::Profile,
|
||||||
|
feature_to_enable: routing_types::SuccessBasedRoutingFeatures,
|
||||||
|
merchant_id: id_type::MerchantId,
|
||||||
|
mut success_based_dynamic_routing_algo: routing_types::DynamicRoutingAlgorithmRef,
|
||||||
|
) -> RouterResult<ApplicationResponse<routing_types::RoutingDictionaryRecord>> {
|
||||||
|
let db = state.store.as_ref();
|
||||||
|
let key_manager_state = &state.into();
|
||||||
|
let profile_id = business_profile.get_id().to_owned();
|
||||||
|
let default_success_based_routing_config = routing_types::SuccessBasedRoutingConfig::default();
|
||||||
|
let algorithm_id = common_utils::generate_routing_id_of_default_length();
|
||||||
|
let timestamp = common_utils::date_time::now();
|
||||||
|
let algo = routing_algorithm::RoutingAlgorithm {
|
||||||
|
algorithm_id: algorithm_id.clone(),
|
||||||
|
profile_id: profile_id.clone(),
|
||||||
|
merchant_id,
|
||||||
|
name: SUCCESS_BASED_DYNAMIC_ROUTING_ALGORITHM.to_string(),
|
||||||
|
description: None,
|
||||||
|
kind: diesel_models::enums::RoutingAlgorithmKind::Dynamic,
|
||||||
|
algorithm_data: serde_json::json!(default_success_based_routing_config),
|
||||||
|
created_at: timestamp,
|
||||||
|
modified_at: timestamp,
|
||||||
|
algorithm_for: common_enums::TransactionType::Payment,
|
||||||
|
};
|
||||||
|
|
||||||
|
let record = db
|
||||||
|
.insert_routing_algorithm(algo)
|
||||||
|
.await
|
||||||
|
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||||
|
.attach_printable("Unable to insert record in routing algorithm table")?;
|
||||||
|
|
||||||
|
success_based_dynamic_routing_algo.update_algorithm_id(algorithm_id, feature_to_enable);
|
||||||
|
update_business_profile_active_dynamic_algorithm_ref(
|
||||||
|
db,
|
||||||
|
key_manager_state,
|
||||||
|
&key_store,
|
||||||
|
business_profile,
|
||||||
|
success_based_dynamic_routing_algo,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let new_record = record.foreign_into();
|
||||||
|
|
||||||
|
core_metrics::ROUTING_CREATE_SUCCESS_RESPONSE.add(
|
||||||
|
&metrics::CONTEXT,
|
||||||
|
1,
|
||||||
|
&add_attributes([("profile_id", profile_id.get_string_repr().to_string())]),
|
||||||
|
);
|
||||||
|
Ok(ApplicationResponse::Json(new_record))
|
||||||
|
}
|
||||||
|
|||||||
@ -942,7 +942,7 @@ pub async fn toggle_success_based_routing(
|
|||||||
) -> impl Responder {
|
) -> impl Responder {
|
||||||
let flow = Flow::ToggleDynamicRouting;
|
let flow = Flow::ToggleDynamicRouting;
|
||||||
let wrapper = routing_types::ToggleSuccessBasedRoutingWrapper {
|
let wrapper = routing_types::ToggleSuccessBasedRoutingWrapper {
|
||||||
status: query.into_inner().status,
|
feature_to_enable: query.into_inner().enable,
|
||||||
profile_id: path.into_inner().profile_id,
|
profile_id: path.into_inner().profile_id,
|
||||||
};
|
};
|
||||||
Box::pin(oss_api::server_wrap(
|
Box::pin(oss_api::server_wrap(
|
||||||
@ -958,7 +958,7 @@ pub async fn toggle_success_based_routing(
|
|||||||
state,
|
state,
|
||||||
auth.merchant_account,
|
auth.merchant_account,
|
||||||
auth.key_store,
|
auth.key_store,
|
||||||
wrapper.status,
|
wrapper.feature_to_enable,
|
||||||
wrapper.profile_id,
|
wrapper.profile_id,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user