diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index df7e96431dbc..c84442437c54 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -25162,9 +25162,19 @@ } ], "nullable": true + }, + "specificity_level": { + "$ref": "#/components/schemas/SuccessRateSpecificityLevel" } } }, + "SuccessRateSpecificityLevel": { + "type": "string", + "enum": [ + "merchant", + "global" + ] + }, "SupportedPaymentMethod": { "type": "object", "required": [ diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs index 2e570816ab4c..8e5027675753 100644 --- a/crates/api_models/src/routing.rs +++ b/crates/api_models/src/routing.rs @@ -779,6 +779,7 @@ impl Default for SuccessBasedRoutingConfig { duration_in_mins: Some(5), max_total_count: Some(2), }), + specificity_level: SuccessRateSpecificityLevel::default(), }), } } @@ -801,6 +802,8 @@ pub struct SuccessBasedRoutingConfigBody { pub default_success_rate: Option, pub max_aggregates_size: Option, pub current_block_threshold: Option, + #[serde(default)] + pub specificity_level: SuccessRateSpecificityLevel, } #[derive(serde::Serialize, serde::Deserialize, Debug, Clone, ToSchema)] @@ -809,6 +812,14 @@ pub struct CurrentBlockThreshold { pub max_total_count: Option, } +#[derive(serde::Serialize, serde::Deserialize, Debug, Default, Clone, ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum SuccessRateSpecificityLevel { + #[default] + Merchant, + Global, +} + #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct SuccessBasedRoutingPayloadWrapper { pub updated_config: SuccessBasedRoutingConfig, @@ -849,6 +860,7 @@ impl SuccessBasedRoutingConfigBody { .as_mut() .map(|threshold| threshold.update(current_block_threshold)); } + self.specificity_level = new.specificity_level } } diff --git a/crates/diesel_models/src/dynamic_routing_stats.rs b/crates/diesel_models/src/dynamic_routing_stats.rs index c055359d8b03..90cf46890806 100644 --- a/crates/diesel_models/src/dynamic_routing_stats.rs +++ b/crates/diesel_models/src/dynamic_routing_stats.rs @@ -20,6 +20,7 @@ pub struct DynamicRoutingStatsNew { pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub created_at: time::PrimitiveDateTime, pub payment_method_type: Option, + pub global_success_based_connector: Option, } #[derive(Clone, Debug, Eq, PartialEq, Queryable, Selectable, Insertable)] @@ -40,4 +41,5 @@ pub struct DynamicRoutingStats { pub conclusive_classification: common_enums::SuccessBasedRoutingConclusiveState, pub created_at: time::PrimitiveDateTime, pub payment_method_type: Option, + pub global_success_based_connector: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 37a39cb731a8..c12062560a20 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -419,6 +419,8 @@ diesel::table! { created_at -> Timestamp, #[max_length = 64] payment_method_type -> Nullable, + #[max_length = 64] + global_success_based_connector -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index 3e27f29427bb..a4669fa906bc 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -431,6 +431,8 @@ diesel::table! { created_at -> Timestamp, #[max_length = 64] payment_method_type -> Nullable, + #[max_length = 64] + global_success_based_connector -> Nullable, } } diff --git a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs index 3cf06ab63beb..511ac4d896b6 100644 --- a/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs +++ b/crates/external_services/src/grpc_client/dynamic_routing/success_rate_client.rs @@ -1,21 +1,24 @@ use api_models::routing::{ CurrentBlockThreshold, RoutableConnectorChoice, RoutableConnectorChoiceWithStatus, - SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, + SuccessBasedRoutingConfig, SuccessBasedRoutingConfigBody, SuccessRateSpecificityLevel, }; use common_utils::{ext_traits::OptionExt, transformers::ForeignTryFrom}; use error_stack::ResultExt; pub use success_rate::{ - success_rate_calculator_client::SuccessRateCalculatorClient, CalSuccessRateConfig, + success_rate_calculator_client::SuccessRateCalculatorClient, CalGlobalSuccessRateConfig, + CalGlobalSuccessRateRequest, CalGlobalSuccessRateResponse, CalSuccessRateConfig, CalSuccessRateRequest, CalSuccessRateResponse, CurrentBlockThreshold as DynamicCurrentThreshold, InvalidateWindowsRequest, - InvalidateWindowsResponse, LabelWithStatus, UpdateSuccessRateWindowConfig, + InvalidateWindowsResponse, LabelWithStatus, + SuccessRateSpecificityLevel as ProtoSpecificityLevel, UpdateSuccessRateWindowConfig, UpdateSuccessRateWindowRequest, UpdateSuccessRateWindowResponse, }; #[allow( missing_docs, unused_qualifications, clippy::unwrap_used, - clippy::as_conversions + clippy::as_conversions, + clippy::use_self )] pub mod success_rate { tonic::include_proto!("success_rate"); @@ -49,6 +52,15 @@ pub trait SuccessBasedDynamicRouting: dyn_clone::DynClone + Send + Sync { id: String, headers: GrpcHeaders, ) -> DynamicRoutingResult; + /// To calculate both global and merchant specific success rate for the list of chosen connectors + async fn calculate_entity_and_global_success_rate( + &self, + id: String, + success_rate_based_config: SuccessBasedRoutingConfig, + params: String, + label_input: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult; } #[async_trait::async_trait] @@ -106,6 +118,7 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { .transpose()?; let labels_with_status = label_input + .clone() .into_iter() .map(|conn_choice| LabelWithStatus { label: conn_choice.routable_connector_choice.to_string(), @@ -113,11 +126,20 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { }) .collect(); + let global_labels_with_status = label_input + .into_iter() + .map(|conn_choice| LabelWithStatus { + label: conn_choice.routable_connector_choice.connector.to_string(), + status: conn_choice.status, + }) + .collect(); + let mut request = tonic::Request::new(UpdateSuccessRateWindowRequest { id, - params, + params: params.clone(), labels_with_status, config, + global_labels_with_status, }); request.add_headers_to_grpc_request(headers); @@ -152,6 +174,52 @@ impl SuccessBasedDynamicRouting for SuccessRateCalculatorClient { .into_inner(); Ok(response) } + + async fn calculate_entity_and_global_success_rate( + &self, + id: String, + success_rate_based_config: SuccessBasedRoutingConfig, + params: String, + label_input: Vec, + headers: GrpcHeaders, + ) -> DynamicRoutingResult { + let labels = label_input + .clone() + .into_iter() + .map(|conn_choice| conn_choice.to_string()) + .collect::>(); + + let global_labels = label_input + .into_iter() + .map(|conn_choice| conn_choice.connector.to_string()) + .collect::>(); + + let config = success_rate_based_config + .config + .map(ForeignTryFrom::foreign_try_from) + .transpose()?; + + let mut request = tonic::Request::new(CalGlobalSuccessRateRequest { + entity_id: id, + entity_params: params, + entity_labels: labels, + global_labels, + config, + }); + + request.add_headers_to_grpc_request(headers); + + let response = self + .clone() + .fetch_entity_and_global_success_rate(request) + .await + .change_context(DynamicRoutingError::SuccessRateBasedRoutingFailure( + "Failed to fetch the entity and global success rate".to_string(), + ))? + .into_inner(); + + Ok(response) + } } impl ForeignTryFrom for DynamicCurrentThreshold { @@ -203,6 +271,30 @@ impl ForeignTryFrom for CalSuccessRateConfig { .change_context(DynamicRoutingError::MissingRequiredField { field: "default_success_rate".to_string(), })?, + specificity_level: match config.specificity_level { + SuccessRateSpecificityLevel::Merchant => Some(ProtoSpecificityLevel::Entity.into()), + SuccessRateSpecificityLevel::Global => Some(ProtoSpecificityLevel::Global.into()), + }, + }) + } +} + +impl ForeignTryFrom for CalGlobalSuccessRateConfig { + type Error = error_stack::Report; + fn foreign_try_from(config: SuccessBasedRoutingConfigBody) -> Result { + Ok(Self { + entity_min_aggregates_size: config + .min_aggregates_size + .get_required_value("min_aggregate_size") + .change_context(DynamicRoutingError::MissingRequiredField { + field: "min_aggregates_size".to_string(), + })?, + entity_default_success_rate: config + .default_success_rate + .get_required_value("default_success_rate") + .change_context(DynamicRoutingError::MissingRequiredField { + field: "default_success_rate".to_string(), + })?, }) } } diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index 2f068b5609b8..d306eff4e4a6 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -624,6 +624,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::routing::StraightThroughAlgorithm, api_models::routing::ConnectorVolumeSplit, api_models::routing::ConnectorSelection, + api_models::routing::SuccessRateSpecificityLevel, api_models::routing::ToggleDynamicRoutingQuery, api_models::routing::ToggleDynamicRoutingPath, api_models::routing::ast::RoutableChoiceKind, diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index 0d66c3b6f17b..9e7dfcf027e7 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -709,7 +709,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( ); let success_based_connectors = client - .calculate_success_rate( + .calculate_entity_and_global_success_rate( business_profile.get_id().get_string_repr().into(), success_based_routing_configs.clone(), success_based_routing_config_params.clone(), @@ -725,8 +725,8 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( let payment_status_attribute = get_desired_payment_status_for_success_routing_metrics(payment_attempt.status); - let first_success_based_connector_label = &success_based_connectors - .labels_with_score + let first_merchant_success_based_connector_label = &success_based_connectors + .entity_scores_with_labels .first() .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable( @@ -735,18 +735,26 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( .label .to_string(); - let (first_success_based_connector, _) = first_success_based_connector_label + let (first_merchant_success_based_connector, _) = first_merchant_success_based_connector_label .split_once(':') .ok_or(errors::ApiErrorResponse::InternalServerError) .attach_printable(format!( "unable to split connector_name and mca_id from the first connector {:?} obtained from dynamic routing service", - first_success_based_connector_label + first_merchant_success_based_connector_label ))?; + let first_global_success_based_connector = &success_based_connectors + .global_scores_with_labels + .first() + .ok_or(errors::ApiErrorResponse::InternalServerError) + .attach_printable( + "unable to fetch the first global connector from list of connectors obtained from dynamic routing service", + )?; + let outcome = get_success_based_metrics_outcome_for_payment( payment_status_attribute, payment_connector.to_string(), - first_success_based_connector.to_string(), + first_merchant_success_based_connector.to_string(), ); let dynamic_routing_stats = DynamicRoutingStatsNew { @@ -755,7 +763,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( merchant_id: payment_attempt.merchant_id.to_owned(), profile_id: payment_attempt.profile_id.to_owned(), amount: payment_attempt.get_total_amount(), - success_based_routing_connector: first_success_based_connector.to_string(), + success_based_routing_connector: first_merchant_success_based_connector.to_string(), payment_connector: payment_connector.to_string(), payment_method_type: payment_attempt.payment_method_type, currency: payment_attempt.currency, @@ -765,6 +773,9 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( payment_status: payment_attempt.status, conclusive_classification: outcome, created_at: common_utils::date_time::now(), + global_success_based_connector: Some( + first_global_success_based_connector.label.to_string(), + ), }; core_metrics::DYNAMIC_SUCCESS_BASED_ROUTING.add( @@ -783,8 +794,16 @@ pub async fn push_metrics_with_update_window_for_success_based_routing( ), ), ( - "success_based_routing_connector", - first_success_based_connector.to_string(), + "merchant_specific_success_based_routing_connector", + first_merchant_success_based_connector.to_string(), + ), + ( + "global_success_based_routing_connector", + first_global_success_based_connector.label.to_string(), + ), + ( + "global_success_based_routing_connector_score", + first_global_success_based_connector.score.to_string(), ), ("payment_connector", payment_connector.to_string()), ( diff --git a/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/down.sql b/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/down.sql new file mode 100644 index 000000000000..c99db9a384d1 --- /dev/null +++ b/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE dynamic_routing_stats +DROP COLUMN IF EXISTS global_success_based_connector; \ No newline at end of file diff --git a/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/up.sql b/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/up.sql new file mode 100644 index 000000000000..81e00a9753c4 --- /dev/null +++ b/migrations/2025-01-07-101337_global_sr_connector_dynamic_routing/up.sql @@ -0,0 +1,3 @@ +-- Your SQL goes here +ALTER TABLE dynamic_routing_stats +ADD COLUMN IF NOT EXISTS global_success_based_connector VARCHAR(64); \ No newline at end of file diff --git a/proto/success_rate.proto b/proto/success_rate.proto index 38e56e36c0ff..4b489e071c35 100644 --- a/proto/success_rate.proto +++ b/proto/success_rate.proto @@ -7,6 +7,8 @@ service SuccessRateCalculator { rpc UpdateSuccessRateWindow (UpdateSuccessRateWindowRequest) returns (UpdateSuccessRateWindowResponse); rpc InvalidateWindows (InvalidateWindowsRequest) returns (InvalidateWindowsResponse); + + rpc FetchEntityAndGlobalSuccessRate (CalGlobalSuccessRateRequest) returns (CalGlobalSuccessRateResponse); } // API-1 types @@ -20,6 +22,12 @@ message CalSuccessRateRequest { message CalSuccessRateConfig { uint32 min_aggregates_size = 1; double default_success_rate = 2; + optional SuccessRateSpecificityLevel specificity_level = 3; +} + +enum SuccessRateSpecificityLevel { + ENTITY = 0; + GLOBAL = 1; } message CalSuccessRateResponse { @@ -31,12 +39,13 @@ message LabelWithScore { string label = 2; } - // API-2 types +// API-2 types message UpdateSuccessRateWindowRequest { string id = 1; string params = 2; repeated LabelWithStatus labels_with_status = 3; UpdateSuccessRateWindowConfig config = 4; + repeated LabelWithStatus global_labels_with_status = 5; } message LabelWithStatus { @@ -65,4 +74,23 @@ message InvalidateWindowsRequest { message InvalidateWindowsResponse { string message = 1; +} + +// API-4 types +message CalGlobalSuccessRateRequest { + string entity_id = 1; + string entity_params = 2; + repeated string entity_labels = 3; + repeated string global_labels = 4; + CalGlobalSuccessRateConfig config = 5; +} + +message CalGlobalSuccessRateConfig { + uint32 entity_min_aggregates_size = 1; + double entity_default_success_rate = 2; +} + +message CalGlobalSuccessRateResponse { + repeated LabelWithScore entity_scores_with_labels = 1; + repeated LabelWithScore global_scores_with_labels = 2; } \ No newline at end of file