From a3cc44c6e15ce69f39104b2ce205bed632e3971e Mon Sep 17 00:00:00 2001 From: Sarthak Soni <76486416+Sarthak1799@users.noreply.github.com> Date: Mon, 23 Jun 2025 22:53:42 +0530 Subject: [PATCH] feat(analytics): Add RoutingApproach filter in payment analytics (#8408) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../clickhouse/scripts/payment_attempts.sql | 4 + crates/analytics/src/core.rs | 5 + crates/analytics/src/payments/core.rs | 1 + crates/analytics/src/payments/distribution.rs | 1 + .../distribution/payment_error_message.rs | 1 + crates/analytics/src/payments/filters.rs | 3 +- crates/analytics/src/payments/metrics.rs | 1 + .../src/payments/metrics/avg_ticket_size.rs | 1 + .../metrics/connector_success_rate.rs | 1 + .../src/payments/metrics/payment_count.rs | 1 + .../metrics/payment_processed_amount.rs | 1 + .../payments/metrics/payment_success_count.rs | 1 + .../src/payments/metrics/retries_count.rs | 1 + .../sessionized_metrics/avg_ticket_size.rs | 1 + .../connector_success_rate.rs | 1 + .../sessionized_metrics/failure_reasons.rs | 1 + .../sessionized_metrics/payment_count.rs | 1 + .../payment_processed_amount.rs | 1 + .../payment_success_count.rs | 1 + .../payments_distribution.rs | 1 + .../sessionized_metrics/retries_count.rs | 1 + .../sessionized_metrics/success_rate.rs | 1 + .../src/payments/metrics/success_rate.rs | 1 + crates/analytics/src/payments/types.rs | 10 ++ crates/analytics/src/query.rs | 5 +- crates/analytics/src/sqlx.rs | 21 ++- crates/analytics/src/types.rs | 1 + crates/analytics/src/utils.rs | 1 + crates/api_models/src/analytics/payments.rs | 9 +- crates/common_enums/src/enums.rs | 43 ++++- crates/diesel_models/src/enums.rs | 3 +- crates/diesel_models/src/payment_attempt.rs | 28 ++++ crates/diesel_models/src/schema.rs | 1 + crates/diesel_models/src/user/sample_data.rs | 2 + .../src/payments/payment_attempt.rs | 8 + crates/router/src/analytics.rs | 32 ++++ crates/router/src/core/debit_routing.rs | 7 +- crates/router/src/core/payments.rs | 46 +++++- crates/router/src/core/payments/helpers.rs | 1 + .../payments/operations/payment_confirm.rs | 1 + .../payments/operations/payment_create.rs | 1 + crates/router/src/core/payments/retry.rs | 1 + crates/router/src/core/payments/routing.rs | 151 +++++++++++++----- .../router/src/core/payments/routing/utils.rs | 12 ++ crates/router/src/core/routing/helpers.rs | 17 +- .../src/services/kafka/payment_attempt.rs | 2 + .../services/kafka/payment_attempt_event.rs | 2 + .../src/types/storage/payment_attempt.rs | 3 + crates/router/src/utils/user/sample_data.rs | 1 + .../src/mock_db/payment_attempt.rs | 1 + .../src/payments/payment_attempt.rs | 5 + .../down.sql | 6 + .../up.sql | 15 ++ .../2025-01-13-081847_drop_v1_columns/up.sql | 3 +- 54 files changed, 406 insertions(+), 65 deletions(-) create mode 100644 migrations/2025-06-19-124558_add_routing_approach_to_attempt/down.sql create mode 100644 migrations/2025-06-19-124558_add_routing_approach_to_attempt/up.sql diff --git a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql index f5d0ca51fd..6673b73fed 100644 --- a/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql +++ b/crates/analytics/docs/clickhouse/scripts/payment_attempts.sql @@ -43,6 +43,7 @@ CREATE TABLE payment_attempt_queue ( `organization_id` String, `profile_id` String, `card_network` Nullable(String), + `routing_approach` LowCardinality(Nullable(String)), `sign_flag` Int8 ) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092', kafka_topic_list = 'hyperswitch-payment-attempt-events', @@ -96,6 +97,7 @@ CREATE TABLE payment_attempts ( `organization_id` String, `profile_id` String, `card_network` Nullable(String), + `routing_approach` LowCardinality(Nullable(String)), `sign_flag` Int8, INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1, INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1, @@ -152,6 +154,7 @@ CREATE MATERIALIZED VIEW payment_attempt_mv TO payment_attempts ( `organization_id` String, `profile_id` String, `card_network` Nullable(String), + `routing_approach` LowCardinality(Nullable(String)), `sign_flag` Int8 ) AS SELECT @@ -200,6 +203,7 @@ SELECT organization_id, profile_id, card_network, + routing_approach, sign_flag FROM payment_attempt_queue diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs index 980e17bc90..f3dd8e8eb8 100644 --- a/crates/analytics/src/core.rs +++ b/crates/analytics/src/core.rs @@ -46,6 +46,11 @@ pub async fn get_domain_info( download_dimensions: None, dimensions: utils::get_dispute_dimensions(), }, + AnalyticsDomain::Routing => GetInfoResponse { + metrics: utils::get_payment_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_dimensions(), + }, }; Ok(info) } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs index 7291d2f1fc..e55ba2726a 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -410,6 +410,7 @@ pub async fn get_filters( PaymentDimensions::CardLast4 => fil.card_last_4, PaymentDimensions::CardIssuer => fil.card_issuer, PaymentDimensions::ErrorReason => fil.error_reason, + PaymentDimensions::RoutingApproach => fil.routing_approach.map(|i| i.as_ref().to_string()), }) .collect::>(); res.query_data.push(FilterValue { diff --git a/crates/analytics/src/payments/distribution.rs b/crates/analytics/src/payments/distribution.rs index 86a2f06c5f..ab82e48f40 100644 --- a/crates/analytics/src/payments/distribution.rs +++ b/crates/analytics/src/payments/distribution.rs @@ -37,6 +37,7 @@ pub struct PaymentDistributionRow { pub total: Option, pub count: Option, pub error_message: Option, + pub routing_approach: Option>, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] diff --git a/crates/analytics/src/payments/distribution/payment_error_message.rs b/crates/analytics/src/payments/distribution/payment_error_message.rs index 0a92cfbe47..d5fa056945 100644 --- a/crates/analytics/src/payments/distribution/payment_error_message.rs +++ b/crates/analytics/src/payments/distribution/payment_error_message.rs @@ -160,6 +160,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/filters.rs b/crates/analytics/src/payments/filters.rs index 668bdaa6c8..b6ebf094e8 100644 --- a/crates/analytics/src/payments/filters.rs +++ b/crates/analytics/src/payments/filters.rs @@ -1,6 +1,6 @@ use api_models::analytics::{payments::PaymentDimensions, Granularity, TimeRange}; use common_utils::errors::ReportSwitchExt; -use diesel_models::enums::{AttemptStatus, AuthenticationType, Currency}; +use diesel_models::enums::{AttemptStatus, AuthenticationType, Currency, RoutingApproach}; use error_stack::ResultExt; use time::PrimitiveDateTime; @@ -65,4 +65,5 @@ pub struct PaymentFilterRow { pub card_issuer: Option, pub error_reason: Option, pub first_attempt: Option, + pub routing_approach: Option>, } diff --git a/crates/analytics/src/payments/metrics.rs b/crates/analytics/src/payments/metrics.rs index 71d2e57d7f..67dc50c515 100644 --- a/crates/analytics/src/payments/metrics.rs +++ b/crates/analytics/src/payments/metrics.rs @@ -50,6 +50,7 @@ pub struct PaymentMetricRow { pub first_attempt: Option, pub total: Option, pub count: Option, + pub routing_approach: Option>, #[serde(with = "common_utils::custom_serde::iso8601::option")] pub start_bucket: Option, #[serde(with = "common_utils::custom_serde::iso8601::option")] diff --git a/crates/analytics/src/payments/metrics/avg_ticket_size.rs b/crates/analytics/src/payments/metrics/avg_ticket_size.rs index 10f350e30c..8ec175cf8d 100644 --- a/crates/analytics/src/payments/metrics/avg_ticket_size.rs +++ b/crates/analytics/src/payments/metrics/avg_ticket_size.rs @@ -122,6 +122,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/connector_success_rate.rs b/crates/analytics/src/payments/metrics/connector_success_rate.rs index 9e6bf31fbf..eb4518f6d2 100644 --- a/crates/analytics/src/payments/metrics/connector_success_rate.rs +++ b/crates/analytics/src/payments/metrics/connector_success_rate.rs @@ -117,6 +117,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/payment_count.rs b/crates/analytics/src/payments/metrics/payment_count.rs index 297aef4fec..ed23a400fb 100644 --- a/crates/analytics/src/payments/metrics/payment_count.rs +++ b/crates/analytics/src/payments/metrics/payment_count.rs @@ -108,6 +108,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs index 9584692466..302acf097e 100644 --- a/crates/analytics/src/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -122,6 +122,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/payment_success_count.rs b/crates/analytics/src/payments/metrics/payment_success_count.rs index 843e285897..d1f437d812 100644 --- a/crates/analytics/src/payments/metrics/payment_success_count.rs +++ b/crates/analytics/src/payments/metrics/payment_success_count.rs @@ -115,6 +115,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs index ced845651c..63ebfaefca 100644 --- a/crates/analytics/src/payments/metrics/retries_count.rs +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -112,6 +112,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/avg_ticket_size.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/avg_ticket_size.rs index e29c19bda8..a513877736 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/avg_ticket_size.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/avg_ticket_size.rs @@ -123,6 +123,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/connector_success_rate.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/connector_success_rate.rs index d8ee7fcb47..626f11aa22 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/connector_success_rate.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/connector_success_rate.rs @@ -118,6 +118,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs index d6944d8d0b..f19185b345 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/failure_reasons.rs @@ -183,6 +183,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_count.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_count.rs index 98735b9383..79d57477e1 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_count.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_count.rs @@ -109,6 +109,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs index 453f988d22..0a2ab1f8a2 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs @@ -140,6 +140,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_success_count.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_success_count.rs index 14391588b4..e59402933a 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_success_count.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_success_count.rs @@ -116,6 +116,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payments_distribution.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payments_distribution.rs index 5c6a8c6ade..d7305cd079 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payments_distribution.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payments_distribution.rs @@ -119,6 +119,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/retries_count.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/retries_count.rs index 7d81c48274..83307d75f7 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/retries_count.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/retries_count.rs @@ -112,6 +112,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/success_rate.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/success_rate.rs index f20308ed3b..8159a615ba 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/success_rate.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/success_rate.rs @@ -112,6 +112,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/metrics/success_rate.rs b/crates/analytics/src/payments/metrics/success_rate.rs index 6698fe8ce6..032f3219a6 100644 --- a/crates/analytics/src/payments/metrics/success_rate.rs +++ b/crates/analytics/src/payments/metrics/success_rate.rs @@ -111,6 +111,7 @@ where i.card_last_4.clone(), i.card_issuer.clone(), i.error_reason.clone(), + i.routing_approach.as_ref().map(|i| i.0), TimeRange { start_time: match (granularity, i.start_bucket) { (Some(g), Some(st)) => g.clip_to_start(st)?, diff --git a/crates/analytics/src/payments/types.rs b/crates/analytics/src/payments/types.rs index b9af3cd061..7bb23d7af7 100644 --- a/crates/analytics/src/payments/types.rs +++ b/crates/analytics/src/payments/types.rs @@ -109,6 +109,16 @@ where .add_filter_in_range_clause("first_attempt", &self.first_attempt) .attach_printable("Error adding first attempt filter")?; } + + if !self.routing_approach.is_empty() { + builder + .add_filter_in_range_clause( + PaymentDimensions::RoutingApproach, + &self.routing_approach, + ) + .attach_printable("Error adding routing approach filter")?; + } + Ok(()) } } diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index 5effd9e70a..50d16c68e6 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -15,7 +15,7 @@ use api_models::{ }, enums::{ AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, IntentStatus, - PaymentMethod, PaymentMethodType, + PaymentMethod, PaymentMethodType, RoutingApproach, }, refunds::RefundStatus, }; @@ -514,7 +514,8 @@ impl_to_sql_for_to_string!( &bool, &u64, u64, - Order + Order, + RoutingApproach ); impl_to_sql_for_to_string!( diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 028a1ddc14..53b9cc10e1 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -13,7 +13,7 @@ use common_utils::{ }; use diesel_models::enums::{ AttemptStatus, AuthenticationType, Currency, FraudCheckStatus, IntentStatus, PaymentMethod, - RefundStatus, + RefundStatus, RoutingApproach, }; use error_stack::ResultExt; use sqlx::{ @@ -103,6 +103,7 @@ db_type!(AuthenticationStatus); db_type!(TransactionStatus); db_type!(AuthenticationConnectors); db_type!(DecoupledAuthenticationType); +db_type!(RoutingApproach); impl<'q, Type> Encode<'q, Postgres> for DBEnumWrapper where @@ -730,6 +731,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let routing_approach: Option> = + row.try_get("routing_approach").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -761,6 +767,7 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow { card_issuer, error_reason, first_attempt, + routing_approach, total, count, start_bucket, @@ -833,6 +840,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributi ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let routing_approach: Option> = + row.try_get("routing_approach").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; let total: Option = row.try_get("total").or_else(|e| match e { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), @@ -875,6 +887,7 @@ impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributi total, count, error_message, + routing_approach, start_bucket, end_bucket, }) @@ -949,6 +962,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow { ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; + let routing_approach: Option> = + row.try_get("routing_approach").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; Ok(Self { currency, status, @@ -965,6 +983,7 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow { card_issuer, error_reason, first_attempt, + routing_approach, }) } } diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 28ff2cec8b..4f9a7655ed 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -21,6 +21,7 @@ pub enum AnalyticsDomain { SdkEvents, ApiEvents, Dispute, + Routing, } #[derive(Debug, strum::AsRefStr, strum::Display, Clone, Copy)] diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 06cc998f3d..36121e56eb 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -24,6 +24,7 @@ pub fn get_payment_dimensions() -> Vec { PaymentDimensions::ProfileId, PaymentDimensions::CardNetwork, PaymentDimensions::MerchantId, + PaymentDimensions::RoutingApproach, ] .into_iter() .map(Into::into) diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 691e827043..537d6df09d 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -8,7 +8,7 @@ use common_utils::id_type; use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::{ AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod, - PaymentMethodType, + PaymentMethodType, RoutingApproach, }; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] @@ -43,6 +43,8 @@ pub struct PaymentFilters { pub error_reason: Vec, #[serde(default)] pub first_attempt: Vec, + #[serde(default)] + pub routing_approach: Vec, } #[derive( @@ -84,6 +86,7 @@ pub enum PaymentDimensions { CardLast4, CardIssuer, ErrorReason, + RoutingApproach, } #[derive( @@ -200,6 +203,7 @@ pub struct PaymentMetricsBucketIdentifier { pub card_last_4: Option, pub card_issuer: Option, pub error_reason: Option, + pub routing_approach: Option, #[serde(rename = "time_range")] pub time_bucket: TimeRange, // Coz FE sucks @@ -225,6 +229,7 @@ impl PaymentMetricsBucketIdentifier { card_last_4: Option, card_issuer: Option, error_reason: Option, + routing_approach: Option, normalized_time_range: TimeRange, ) -> Self { Self { @@ -242,6 +247,7 @@ impl PaymentMetricsBucketIdentifier { card_last_4, card_issuer, error_reason, + routing_approach, time_bucket: normalized_time_range, start_time: normalized_time_range.start_time, } @@ -264,6 +270,7 @@ impl Hash for PaymentMetricsBucketIdentifier { self.card_last_4.hash(state); self.card_issuer.hash(state); self.error_reason.hash(state); + self.routing_approach.map(|i| i.to_string()).hash(state); self.time_bucket.hash(state); } } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 641dc83291..d7f4a1b2a2 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -29,7 +29,7 @@ pub mod diesel_exports { DbPaymentType as PaymentType, DbProcessTrackerStatus as ProcessTrackerStatus, DbRefundStatus as RefundStatus, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, - DbScaExemptionType as ScaExemptionType, + DbRoutingApproach as RoutingApproach, DbScaExemptionType as ScaExemptionType, DbSuccessBasedRoutingConclusiveState as SuccessBasedRoutingConclusiveState, DbTokenizationFlag as TokenizationFlag, DbWebhookDeliveryAttempt as WebhookDeliveryAttempt, }; @@ -8494,3 +8494,44 @@ pub enum TokenDataType { /// Fetch network token for the given payment method NetworkToken, } + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + Hash, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::VariantNames, + strum::EnumIter, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "db_enum")] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum RoutingApproach { + SuccessRateExploitation, + SuccessRateExploration, + ContractBasedRouting, + DebitRouting, + RuleBasedRouting, + VolumeBasedRouting, + #[default] + DefaultFallback, +} + +impl RoutingApproach { + pub fn from_decision_engine_approach(approach: &str) -> Self { + match approach { + "SR_SELECTION_V3_ROUTING" => Self::SuccessRateExploitation, + "SR_V3_HEDGING" => Self::SuccessRateExploration, + "NTW_BASED_ROUTING" => Self::DebitRouting, + _ => Self::DefaultFallback, + } + } +} diff --git a/crates/diesel_models/src/enums.rs b/crates/diesel_models/src/enums.rs index fc9417342e..f7c84368b4 100644 --- a/crates/diesel_models/src/enums.rs +++ b/crates/diesel_models/src/enums.rs @@ -21,7 +21,8 @@ pub mod diesel_exports { DbRelayType as RelayType, DbRequestIncrementalAuthorization as RequestIncrementalAuthorization, DbRevenueRecoveryAlgorithmType as RevenueRecoveryAlgorithmType, DbRoleScope as RoleScope, - DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbScaExemptionType as ScaExemptionType, + DbRoutingAlgorithmKind as RoutingAlgorithmKind, DbRoutingApproach as RoutingApproach, + DbScaExemptionType as ScaExemptionType, DbSuccessBasedRoutingConclusiveState as SuccessBasedRoutingConclusiveState, DbTokenizationFlag as TokenizationFlag, DbTotpStatus as TotpStatus, DbTransactionType as TransactionType, DbUserRoleVersion as UserRoleVersion, diff --git a/crates/diesel_models/src/payment_attempt.rs b/crates/diesel_models/src/payment_attempt.rs index 473502ad74..d438b09351 100644 --- a/crates/diesel_models/src/payment_attempt.rs +++ b/crates/diesel_models/src/payment_attempt.rs @@ -203,6 +203,7 @@ pub struct PaymentAttempt { pub processor_merchant_id: Option, pub created_by: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -422,6 +423,7 @@ pub struct PaymentAttemptNew { pub processor_merchant_id: Option, pub created_by: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -495,6 +497,7 @@ pub enum PaymentAttemptUpdate { order_tax_amount: Option, connector_mandate_detail: Option, card_discovery: Option, + routing_approach: Option, }, VoidUpdate { status: storage_enums::AttemptStatus, @@ -937,6 +940,7 @@ pub struct PaymentAttemptUpdateInternal { pub issuer_error_code: Option, pub issuer_error_message: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -1126,6 +1130,7 @@ impl PaymentAttemptUpdate { issuer_error_code, issuer_error_message, setup_future_usage_applied, + routing_approach, } = PaymentAttemptUpdateInternal::from(self).populate_derived_fields(&source); PaymentAttempt { amount: amount.unwrap_or(source.amount), @@ -1192,6 +1197,7 @@ impl PaymentAttemptUpdate { issuer_error_message: issuer_error_message.or(source.issuer_error_message), setup_future_usage_applied: setup_future_usage_applied .or(source.setup_future_usage_applied), + routing_approach: routing_approach.or(source.routing_approach), ..source } } @@ -2250,6 +2256,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::AuthenticationTypeUpdate { authentication_type, @@ -2312,6 +2319,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::ConfirmUpdate { amount, @@ -2348,6 +2356,7 @@ impl From for PaymentAttemptUpdateInternal { order_tax_amount, connector_mandate_detail, card_discovery, + routing_approach, } => Self { amount: Some(amount), currency: Some(currency), @@ -2406,6 +2415,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach, }, PaymentAttemptUpdate::VoidUpdate { status, @@ -2469,6 +2479,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::RejectUpdate { status, @@ -2533,6 +2544,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::BlocklistUpdate { status, @@ -2597,6 +2609,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::ConnectorMandateDetailUpdate { connector_mandate_detail, @@ -2659,6 +2672,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::PaymentMethodDetailsUpdate { payment_method_id, @@ -2721,6 +2735,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::ResponseUpdate { status, @@ -2811,6 +2826,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied, + routing_approach: None, } } PaymentAttemptUpdate::ErrorUpdate { @@ -2892,6 +2908,7 @@ impl From for PaymentAttemptUpdateInternal { card_discovery: None, charges: None, setup_future_usage_applied: None, + routing_approach: None, } } PaymentAttemptUpdate::StatusUpdate { status, updated_by } => Self { @@ -2952,6 +2969,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::UpdateTrackers { payment_token, @@ -3020,6 +3038,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::UnresolvedResponseUpdate { status, @@ -3095,6 +3114,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, } } PaymentAttemptUpdate::PreprocessingUpdate { @@ -3169,6 +3189,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, } } PaymentAttemptUpdate::CaptureUpdate { @@ -3233,6 +3254,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::AmountToCaptureUpdate { status, @@ -3296,6 +3318,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::ConnectorResponse { authentication_data, @@ -3368,6 +3391,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, } } PaymentAttemptUpdate::IncrementalAuthorizationAmountUpdate { @@ -3431,6 +3455,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::AuthenticationUpdate { status, @@ -3496,6 +3521,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, PaymentAttemptUpdate::ManualUpdate { status, @@ -3570,6 +3596,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, } } PaymentAttemptUpdate::PostSessionTokensUpdate { @@ -3633,6 +3660,7 @@ impl From for PaymentAttemptUpdateInternal { issuer_error_code: None, issuer_error_message: None, setup_future_usage_applied: None, + routing_approach: None, }, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 522ad271be..cb690e3f60 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -950,6 +950,7 @@ diesel::table! { #[max_length = 255] created_by -> Nullable, setup_future_usage_applied -> Nullable, + routing_approach -> Nullable, } } diff --git a/crates/diesel_models/src/user/sample_data.rs b/crates/diesel_models/src/user/sample_data.rs index 3e445bb502..2a21bb7c40 100644 --- a/crates/diesel_models/src/user/sample_data.rs +++ b/crates/diesel_models/src/user/sample_data.rs @@ -216,6 +216,7 @@ pub struct PaymentAttemptBatchNew { pub processor_merchant_id: Option, pub created_by: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -301,6 +302,7 @@ impl PaymentAttemptBatchNew { processor_merchant_id: self.processor_merchant_id, created_by: self.created_by, setup_future_usage_applied: self.setup_future_usage_applied, + routing_approach: self.routing_approach, } } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs index cdc9103c86..1de29f20d4 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_attempt.rs @@ -908,6 +908,7 @@ pub struct PaymentAttempt { /// merchantwho invoked the resource based api (identifier) and through what source (Api, Jwt(Dashboard)) pub created_by: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -1162,6 +1163,7 @@ pub struct PaymentAttemptNew { /// merchantwho invoked the resource based api (identifier) and through what source (Api, Jwt(Dashboard)) pub created_by: Option, pub setup_future_usage_applied: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -1229,6 +1231,7 @@ pub enum PaymentAttemptUpdate { customer_acceptance: Option, connector_mandate_detail: Option, card_discovery: Option, + routing_approach: Option, // where all to add this one }, RejectUpdate { status: storage_enums::AttemptStatus, @@ -1487,6 +1490,7 @@ impl PaymentAttemptUpdate { customer_acceptance, connector_mandate_detail, card_discovery, + routing_approach, } => DieselPaymentAttemptUpdate::ConfirmUpdate { amount: net_amount.get_order_amount(), currency, @@ -1522,6 +1526,7 @@ impl PaymentAttemptUpdate { order_tax_amount: net_amount.get_order_tax_amount(), connector_mandate_detail, card_discovery, + routing_approach, }, Self::VoidUpdate { status, @@ -1923,6 +1928,7 @@ impl behaviour::Conversion for PaymentAttempt { connector_transaction_data: None, processor_merchant_id: Some(self.processor_merchant_id), created_by: self.created_by.map(|cb| cb.to_string()), + routing_approach: self.routing_approach, }) } @@ -2018,6 +2024,7 @@ impl behaviour::Conversion for PaymentAttempt { .created_by .and_then(|created_by| created_by.parse::().ok()), setup_future_usage_applied: storage_model.setup_future_usage_applied, + routing_approach: storage_model.routing_approach, }) } .await @@ -2106,6 +2113,7 @@ impl behaviour::Conversion for PaymentAttempt { processor_merchant_id: Some(self.processor_merchant_id), created_by: self.created_by.map(|cb| cb.to_string()), setup_future_usage_applied: self.setup_future_usage_applied, + routing_approach: self.routing_approach, }) } } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index ffac49e7a8..e40bce1233 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -62,6 +62,10 @@ pub mod routes { web::resource("metrics/payments") .route(web::post().to(get_merchant_payment_metrics)), ) + .service( + web::resource("metrics/routing") + .route(web::post().to(get_merchant_payment_metrics)), + ) .service( web::resource("metrics/refunds") .route(web::post().to(get_merchant_refund_metrics)), @@ -70,6 +74,10 @@ pub mod routes { web::resource("filters/payments") .route(web::post().to(get_merchant_payment_filters)), ) + .service( + web::resource("filters/routing") + .route(web::post().to(get_merchant_payment_filters)), + ) .service( web::resource("filters/frm").route(web::post().to(get_frm_filters)), ) @@ -175,6 +183,10 @@ pub mod routes { web::resource("metrics/payments") .route(web::post().to(get_merchant_payment_metrics)), ) + .service( + web::resource("metrics/routing") + .route(web::post().to(get_merchant_payment_metrics)), + ) .service( web::resource("metrics/refunds") .route(web::post().to(get_merchant_refund_metrics)), @@ -183,6 +195,10 @@ pub mod routes { web::resource("filters/payments") .route(web::post().to(get_merchant_payment_filters)), ) + .service( + web::resource("filters/routing") + .route(web::post().to(get_merchant_payment_filters)), + ) .service( web::resource("filters/refunds") .route(web::post().to(get_merchant_refund_filters)), @@ -241,6 +257,14 @@ pub mod routes { web::resource("filters/payments") .route(web::post().to(get_org_payment_filters)), ) + .service( + web::resource("metrics/routing") + .route(web::post().to(get_org_payment_metrics)), + ) + .service( + web::resource("filters/routing") + .route(web::post().to(get_org_payment_filters)), + ) .service( web::resource("metrics/refunds") .route(web::post().to(get_org_refund_metrics)), @@ -291,6 +315,14 @@ pub mod routes { web::resource("filters/payments") .route(web::post().to(get_profile_payment_filters)), ) + .service( + web::resource("metrics/routing") + .route(web::post().to(get_profile_payment_metrics)), + ) + .service( + web::resource("filters/routing") + .route(web::post().to(get_profile_payment_filters)), + ) .service( web::resource("metrics/refunds") .route(web::post().to(get_profile_refund_metrics)), diff --git a/crates/router/src/core/debit_routing.rs b/crates/router/src/core/debit_routing.rs index ca03f5d253..4975ffc4bd 100644 --- a/crates/router/src/core/debit_routing.rs +++ b/crates/router/src/core/debit_routing.rs @@ -306,15 +306,14 @@ where } pub async fn get_debit_routing_output< - F: Clone, - D: OperationSessionGetters + OperationSessionSetters, + F: Clone + Send, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, >( state: &SessionState, payment_data: &mut D, acquirer_country: enums::CountryAlpha2, ) -> Option { logger::debug!("Fetching sorted card networks"); - let payment_attempt = payment_data.get_payment_attempt(); let (saved_co_badged_card_data, saved_card_type, card_isin) = extract_saved_card_info(payment_data); @@ -355,9 +354,9 @@ pub async fn get_debit_routing_output< routing::perform_open_routing_for_debit_routing( state, - payment_attempt, co_badged_card_request, card_isin, + payment_data, ) .await .map_err(|error| { diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index d78e8f6b57..30e49f9def 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -8000,7 +8000,7 @@ pub async fn route_connector_v2_for_payments( .as_ref() .or(business_profile.routing_algorithm_id.as_ref()); - let connectors = routing::perform_static_routing_v1( + let (connectors, _) = routing::perform_static_routing_v1( state, merchant_context.get_merchant_account().get_id(), routing_algorithm_id, @@ -8067,7 +8067,7 @@ where algorithm_ref.algorithm_id }; - let connectors = routing::perform_static_routing_v1( + let (connectors, routing_approach) = routing::perform_static_routing_v1( state, merchant_context.get_merchant_account().get_id(), routing_algorithm_id.as_ref(), @@ -8077,6 +8077,8 @@ where .await .change_context(errors::ApiErrorResponse::InternalServerError)?; + payment_data.set_routing_approach_in_attempt(routing_approach); + #[cfg(all(feature = "v1", feature = "dynamic_routing"))] let payment_attempt = transaction_data.payment_attempt.clone(); @@ -8125,6 +8127,7 @@ where connectors.clone(), business_profile, payment_attempt, + payment_data, ) .await .map_err(|e| logger::error!(open_routing_error=?e)) @@ -8167,7 +8170,7 @@ where connectors.clone(), business_profile, dynamic_routing_config_params_interpolator, - payment_data.get_payment_attempt(), + payment_data, ) .await .map_err(|e| logger::error!(dynamic_routing_error=?e)) @@ -8244,7 +8247,7 @@ pub async fn route_connector_v1_for_payouts( algorithm_ref.algorithm_id }; - let connectors = routing::perform_static_routing_v1( + let (connectors, _) = routing::perform_static_routing_v1( state, merchant_context.get_merchant_account().get_id(), routing_algorithm_id.as_ref(), @@ -8969,6 +8972,7 @@ pub trait OperationSessionSetters { &mut self, external_vault_session_details: Option, ); + fn set_routing_approach_in_attempt(&mut self, routing_approach: Option); } #[cfg(feature = "v1")] @@ -9268,6 +9272,13 @@ impl OperationSessionSetters for PaymentData { fn set_vault_operation(&mut self, vault_operation: domain_payments::VaultOperation) { self.vault_operation = Some(vault_operation); } + + fn set_routing_approach_in_attempt( + &mut self, + routing_approach: Option, + ) { + self.payment_attempt.routing_approach = routing_approach; + } } #[cfg(feature = "v2")] @@ -9554,6 +9565,13 @@ impl OperationSessionSetters for PaymentIntentData { ) { self.vault_session_details = vault_session_details; } + + fn set_routing_approach_in_attempt( + &mut self, + routing_approach: Option, + ) { + todo!() + } } #[cfg(feature = "v2")] @@ -9843,6 +9861,13 @@ impl OperationSessionSetters for PaymentConfirmData { ) { todo!() } + + fn set_routing_approach_in_attempt( + &mut self, + routing_approach: Option, + ) { + todo!() + } } #[cfg(feature = "v2")] @@ -10128,6 +10153,12 @@ impl OperationSessionSetters for PaymentStatusData { ) { todo!() } + fn set_routing_approach_in_attempt( + &mut self, + routing_approach: Option, + ) { + todo!() + } } #[cfg(feature = "v2")] @@ -10414,6 +10445,13 @@ impl OperationSessionSetters for PaymentCaptureData { ) { todo!() } + + fn set_routing_approach_in_attempt( + &mut self, + routing_approach: Option, + ) { + todo!() + } } #[cfg(feature = "v2")] diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index 5b7c8041ce..251b90a60d 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -4398,6 +4398,7 @@ impl AttemptType { processor_merchant_id: old_payment_attempt.processor_merchant_id, created_by: old_payment_attempt.created_by, setup_future_usage_applied: None, + routing_approach: old_payment_attempt.routing_approach, } } diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 4f4ca16b8d..fe4e63232b 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -1899,6 +1899,7 @@ impl UpdateTracker, api::PaymentsRequest> for .payment_attempt .connector_mandate_detail, card_discovery, + routing_approach: payment_data.payment_attempt.routing_approach, }, storage_scheme, ) diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 68f1b9d9c1..43e9c39c56 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -1362,6 +1362,7 @@ impl PaymentCreate { processor_merchant_id: merchant_id.to_owned(), created_by: None, setup_future_usage_applied: request.setup_future_usage, + routing_approach: Some(common_enums::RoutingApproach::default()) }, additional_pm_data, diff --git a/crates/router/src/core/payments/retry.rs b/crates/router/src/core/payments/retry.rs index a4d1cc5151..1cc886be82 100644 --- a/crates/router/src/core/payments/retry.rs +++ b/crates/router/src/core/payments/retry.rs @@ -702,6 +702,7 @@ pub fn make_new_payment_attempt( processor_merchant_id: old_payment_attempt.processor_merchant_id, created_by: old_payment_attempt.created_by, setup_future_usage_applied: setup_future_usage_intent, // setup future usage is picked from intent for new payment attempt + routing_approach: old_payment_attempt.routing_approach, } } diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs index 2ab004cccd..ef7d1d46f5 100644 --- a/crates/router/src/core/payments/routing.rs +++ b/crates/router/src/core/payments/routing.rs @@ -59,7 +59,12 @@ use crate::core::routing::transformers::OpenRouterDecideGatewayRequestExt; use crate::routes::app::SessionStateInfo; use crate::{ core::{ - errors, errors as oss_errors, payments::routing::utils::DecisionEngineApiHandler, routing, + errors, errors as oss_errors, + payments::{ + routing::utils::DecisionEngineApiHandler, OperationSessionGetters, + OperationSessionSetters, + }, + routing, }, logger, services, types::{ @@ -432,7 +437,10 @@ pub async fn perform_static_routing_v1( algorithm_id: Option<&common_utils::id_type::RoutingId>, business_profile: &domain::Profile, transaction_data: &routing::TransactionData<'_>, -) -> RoutingResult> { +) -> RoutingResult<( + Vec, + Option, +)> { let algorithm_id = if let Some(id) = algorithm_id { id } else { @@ -449,7 +457,7 @@ pub async fn perform_static_routing_v1( .get_default_fallback_list_of_connector_under_profile() .change_context(errors::RoutingError::FallbackConfigFetchFailed)?; - return Ok(fallback_config); + return Ok((fallback_config, None)); }; let cached_algorithm = ensure_algorithm_cached_v1( state, @@ -503,14 +511,18 @@ pub async fn perform_static_routing_v1( logger::error!(decision_engine_euclid_evaluate_error=?e, "decision_engine_euclid: error in evaluation of rule") ).unwrap_or_default(); - let routable_connectors = match cached_algorithm.as_ref() { - CachedAlgorithm::Single(conn) => vec![(**conn).clone()], - CachedAlgorithm::Priority(plist) => plist.clone(), - CachedAlgorithm::VolumeSplit(splits) => perform_volume_split(splits.to_vec()) - .change_context(errors::RoutingError::ConnectorSelectionFailed)?, - CachedAlgorithm::Advanced(interpreter) => { - execute_dsl_and_get_connector_v1(backend_input, interpreter)? - } + let (routable_connectors, routing_approach) = match cached_algorithm.as_ref() { + CachedAlgorithm::Single(conn) => (vec![(**conn).clone()], None), + CachedAlgorithm::Priority(plist) => (plist.clone(), None), + CachedAlgorithm::VolumeSplit(splits) => ( + perform_volume_split(splits.to_vec()) + .change_context(errors::RoutingError::ConnectorSelectionFailed)?, + Some(common_enums::RoutingApproach::VolumeBasedRouting), + ), + CachedAlgorithm::Advanced(interpreter) => ( + execute_dsl_and_get_connector_v1(backend_input, interpreter)?, + Some(common_enums::RoutingApproach::RuleBasedRouting), + ), }; utils::compare_and_log_result( @@ -519,13 +531,16 @@ pub async fn perform_static_routing_v1( "evaluate_routing".to_string(), ); - Ok(utils::select_routing_result( - state, - business_profile, - routable_connectors, - de_euclid_connectors, - ) - .await) + Ok(( + utils::select_routing_result( + state, + business_profile, + routable_connectors, + de_euclid_connectors, + ) + .await, + routing_approach, + )) } async fn ensure_algorithm_cached_v1( @@ -1573,12 +1588,17 @@ pub fn make_dsl_input_for_surcharge( } #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -pub async fn perform_dynamic_routing_with_open_router( +pub async fn perform_dynamic_routing_with_open_router( state: &SessionState, routable_connectors: Vec, profile: &domain::Profile, payment_data: oss_storage::PaymentAttempt, -) -> RoutingResult> { + old_payment_data: &mut D, +) -> RoutingResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ let dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = profile .dynamic_routing_algorithm .clone() @@ -1610,6 +1630,7 @@ pub async fn perform_dynamic_routing_with_open_router( profile.get_id(), &payment_data, is_elimination_enabled, + old_payment_data, ) .await?; @@ -1642,12 +1663,18 @@ pub async fn perform_dynamic_routing_with_open_router( } #[cfg(feature = "v1")] -pub async fn perform_open_routing_for_debit_routing( +pub async fn perform_open_routing_for_debit_routing( state: &SessionState, - payment_attempt: &oss_storage::PaymentAttempt, co_badged_card_request: or_types::CoBadgedCardRequest, card_isin: Option>, -) -> RoutingResult { + old_payment_data: &mut D, +) -> RoutingResult +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ + let payment_attempt = old_payment_data.get_payment_attempt().clone(); + logger::debug!( "performing debit routing with open_router for profile {}", payment_attempt.profile_id.get_string_repr() @@ -1661,7 +1688,7 @@ pub async fn perform_open_routing_for_debit_routing( ); let open_router_req_body = OpenRouterDecideGatewayRequest::construct_debit_request( - payment_attempt, + &payment_attempt, metadata, card_isin, Some(or_types::RankingAlgorithm::NtwBasedRouting), @@ -1692,11 +1719,14 @@ pub async fn perform_open_routing_for_debit_routing( let output = match response { Ok(events_response) => { - let debit_routing_output = events_response - .response - .ok_or(errors::RoutingError::OpenRouterError( - "Response from decision engine API is empty".to_string(), - ))? + let response = + events_response + .response + .ok_or(errors::RoutingError::OpenRouterError( + "Response from decision engine API is empty".to_string(), + ))?; + + let debit_routing_output = response .debit_routing_output .get_required_value("debit_routing_output") .change_context(errors::RoutingError::OpenRouterError( @@ -1704,6 +1734,12 @@ pub async fn perform_open_routing_for_debit_routing( )) .attach_printable("debit_routing_output is missing in the open routing response")?; + old_payment_data.set_routing_approach_in_attempt(Some( + common_enums::RoutingApproach::from_decision_engine_approach( + &response.routing_approach, + ), + )); + Ok(debit_routing_output) } Err(error_response) => { @@ -1718,13 +1754,17 @@ pub async fn perform_open_routing_for_debit_routing( } #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -pub async fn perform_dynamic_routing_with_intelligent_router( +pub async fn perform_dynamic_routing_with_intelligent_router( state: &SessionState, routable_connectors: Vec, profile: &domain::Profile, dynamic_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, - payment_attempt: &oss_storage::PaymentAttempt, -) -> RoutingResult> { + payment_data: &mut D, +) -> RoutingResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ let dynamic_routing_algo_ref: api_routing::DynamicRoutingAlgorithmRef = profile .dynamic_routing_algorithm .clone() @@ -1744,6 +1784,8 @@ pub async fn perform_dynamic_routing_with_intelligent_router( profile.get_id().get_string_repr() ); + let payment_attempt = payment_data.get_payment_attempt().clone(); + let mut connector_list = match dynamic_routing_algo_ref .success_based_algorithm .as_ref() @@ -1756,6 +1798,7 @@ pub async fn perform_dynamic_routing_with_intelligent_router( &payment_attempt.payment_id, dynamic_routing_config_params_interpolator.clone(), algorithm.clone(), + payment_data, ) }) .await @@ -1779,6 +1822,7 @@ pub async fn perform_dynamic_routing_with_intelligent_router( &payment_attempt.payment_id, dynamic_routing_config_params_interpolator.clone(), algorithm.clone(), + payment_data, ) }) .await @@ -1816,13 +1860,18 @@ pub async fn perform_dynamic_routing_with_intelligent_router( #[cfg(all(feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] -pub async fn perform_decide_gateway_call_with_open_router( +pub async fn perform_decide_gateway_call_with_open_router( state: &SessionState, mut routable_connectors: Vec, profile_id: &common_utils::id_type::ProfileId, payment_attempt: &oss_storage::PaymentAttempt, is_elimination_enabled: bool, -) -> RoutingResult> { + old_payment_data: &mut D, +) -> RoutingResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ logger::debug!( "performing decide_gateway call with open_router for profile {}", profile_id.get_string_repr() @@ -1879,6 +1928,12 @@ pub async fn perform_decide_gateway_call_with_open_router( .to_string(), ); + old_payment_data.set_routing_approach_in_attempt(Some( + common_enums::RoutingApproach::from_decision_engine_approach( + &decided_gateway.routing_approach, + ), + )); + if let Some(gateway_priority_map) = decided_gateway.gateway_priority_map { logger::debug!(gateway_priority_map=?gateway_priority_map, routing_approach=decided_gateway.routing_approach, "open_router decide_gateway call response"); routable_connectors.sort_by(|connector_choice_a, connector_choice_b| { @@ -1992,7 +2047,8 @@ pub async fn update_gateway_score_with_open_router( /// success based dynamic routing #[cfg(all(feature = "v1", feature = "dynamic_routing"))] #[instrument(skip_all)] -pub async fn perform_success_based_routing( +#[allow(clippy::too_many_arguments)] +pub async fn perform_success_based_routing( state: &SessionState, routable_connectors: Vec, profile_id: &common_utils::id_type::ProfileId, @@ -2000,7 +2056,12 @@ pub async fn perform_success_based_routing( payment_id: &common_utils::id_type::PaymentId, success_based_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, success_based_algo_ref: api_routing::SuccessBasedAlgorithm, -) -> RoutingResult> { + payment_data: &mut D, +) -> RoutingResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ if success_based_algo_ref.enabled_feature == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection { @@ -2131,6 +2192,9 @@ pub async fn perform_success_based_routing( })?; routing_event.set_routing_approach(success_based_connectors.routing_approach.to_string()); + payment_data.set_routing_approach_in_attempt(Some(common_enums::RoutingApproach::from( + success_based_connectors.routing_approach, + ))); let mut connectors = Vec::with_capacity(success_based_connectors.labels_with_score.len()); for label_with_score in success_based_connectors.labels_with_score { @@ -2367,7 +2431,8 @@ pub async fn perform_elimination_routing( } #[cfg(all(feature = "v1", feature = "dynamic_routing"))] -pub async fn perform_contract_based_routing( +#[allow(clippy::too_many_arguments)] +pub async fn perform_contract_based_routing( state: &SessionState, routable_connectors: Vec, profile_id: &common_utils::id_type::ProfileId, @@ -2375,7 +2440,12 @@ pub async fn perform_contract_based_routing( payment_id: &common_utils::id_type::PaymentId, _dynamic_routing_config_params_interpolator: routing::helpers::DynamicRoutingConfigParamsInterpolator, contract_based_algo_ref: api_routing::ContractRoutingAlgorithm, -) -> RoutingResult> { + payment_data: &mut D, +) -> RoutingResult> +where + F: Send + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, +{ if contract_based_algo_ref.enabled_feature == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection { @@ -2528,6 +2598,10 @@ pub async fn perform_contract_based_routing( status_code: 500, })?; + payment_data.set_routing_approach_in_attempt(Some( + common_enums::RoutingApproach::ContractBasedRouting, + )); + let mut connectors = Vec::with_capacity(contract_based_connectors.labels_with_score.len()); for label_with_score in contract_based_connectors.labels_with_score { @@ -2565,6 +2639,7 @@ pub async fn perform_contract_based_routing( routing_event.set_status_code(200); routing_event.set_routable_connectors(connectors.clone()); + routing_event.set_routing_approach(api_routing::RoutingApproach::ContractBased.to_string()); state.event_handler().log_event(&routing_event); Ok(connectors) } else { diff --git a/crates/router/src/core/payments/routing/utils.rs b/crates/router/src/core/payments/routing/utils.rs index 4296fea819..0b74273419 100644 --- a/crates/router/src/core/payments/routing/utils.rs +++ b/crates/router/src/core/payments/routing/utils.rs @@ -1522,6 +1522,18 @@ impl RoutingApproach { } } +impl From for common_enums::RoutingApproach { + fn from(approach: RoutingApproach) -> Self { + match approach { + RoutingApproach::Exploitation => Self::SuccessRateExploitation, + RoutingApproach::Exploration => Self::SuccessRateExploration, + RoutingApproach::ContractBased => Self::ContractBasedRouting, + RoutingApproach::StaticRouting => Self::RuleBasedRouting, + _ => Self::DefaultFallback, + } + } +} + impl std::fmt::Display for RoutingApproach { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index e73c9c5bc7..6d263eece2 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -54,7 +54,10 @@ use crate::{ }; #[cfg(feature = "v1")] use crate::{ - core::payments::routing::utils::{self as routing_utils, DecisionEngineApiHandler}, + core::payments::{ + routing::utils::{self as routing_utils, DecisionEngineApiHandler}, + OperationSessionGetters, OperationSessionSetters, + }, services, }; #[cfg(all(feature = "dynamic_routing", feature = "v1"))] @@ -338,11 +341,7 @@ impl RoutingDecisionData { pub fn apply_routing_decision(&self, payment_data: &mut D) where F: Send + Clone, - D: crate::core::payments::OperationSessionGetters - + crate::core::payments::OperationSessionSetters - + Send - + Sync - + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, { match self { Self::DebitRouting(data) => data.apply_debit_routing_decision(payment_data), @@ -364,11 +363,7 @@ impl DebitRoutingDecisionData { pub fn apply_debit_routing_decision(&self, payment_data: &mut D) where F: Send + Clone, - D: crate::core::payments::OperationSessionGetters - + crate::core::payments::OperationSessionSetters - + Send - + Sync - + Clone, + D: OperationSessionGetters + OperationSessionSetters + Send + Sync + Clone, { payment_data.set_card_network(self.card_network.clone()); self.debit_routing_result diff --git a/crates/router/src/services/kafka/payment_attempt.rs b/crates/router/src/services/kafka/payment_attempt.rs index f2c18ae930..68f1d01351 100644 --- a/crates/router/src/services/kafka/payment_attempt.rs +++ b/crates/router/src/services/kafka/payment_attempt.rs @@ -69,6 +69,7 @@ pub struct KafkaPaymentAttempt<'a> { pub organization_id: &'a id_type::OrganizationId, pub card_network: Option, pub card_discovery: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -130,6 +131,7 @@ impl<'a> KafkaPaymentAttempt<'a> { card_discovery: attempt .card_discovery .map(|discovery| discovery.to_string()), + routing_approach: attempt.routing_approach, } } } diff --git a/crates/router/src/services/kafka/payment_attempt_event.rs b/crates/router/src/services/kafka/payment_attempt_event.rs index e06f4e014d..9bfc6f8c31 100644 --- a/crates/router/src/services/kafka/payment_attempt_event.rs +++ b/crates/router/src/services/kafka/payment_attempt_event.rs @@ -70,6 +70,7 @@ pub struct KafkaPaymentAttemptEvent<'a> { pub organization_id: &'a id_type::OrganizationId, pub card_network: Option, pub card_discovery: Option, + pub routing_approach: Option, } #[cfg(feature = "v1")] @@ -131,6 +132,7 @@ impl<'a> KafkaPaymentAttemptEvent<'a> { card_discovery: attempt .card_discovery .map(|discovery| discovery.to_string()), + routing_approach: attempt.routing_approach, } } } diff --git a/crates/router/src/types/storage/payment_attempt.rs b/crates/router/src/types/storage/payment_attempt.rs index 3a942f70e3..5289ed02af 100644 --- a/crates/router/src/types/storage/payment_attempt.rs +++ b/crates/router/src/types/storage/payment_attempt.rs @@ -225,6 +225,7 @@ mod tests { processor_merchant_id: Default::default(), created_by: None, setup_future_usage_applied: Default::default(), + routing_approach: Default::default(), }; let store = state @@ -315,6 +316,7 @@ mod tests { processor_merchant_id: Default::default(), created_by: None, setup_future_usage_applied: Default::default(), + routing_approach: Default::default(), }; let store = state .stores @@ -418,6 +420,7 @@ mod tests { processor_merchant_id: Default::default(), created_by: None, setup_future_usage_applied: Default::default(), + routing_approach: Default::default(), }; let store = state .stores diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 989e7db3c0..52c3d29482 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -381,6 +381,7 @@ pub async fn generate_sample_data( processor_merchant_id: Some(merchant_id.clone()), created_by: None, setup_future_usage_applied: None, + routing_approach: None, }; let refund = if refunds_count < number_of_refunds && !is_failed_payment { diff --git a/crates/storage_impl/src/mock_db/payment_attempt.rs b/crates/storage_impl/src/mock_db/payment_attempt.rs index feec3a1f3a..0de343b94e 100644 --- a/crates/storage_impl/src/mock_db/payment_attempt.rs +++ b/crates/storage_impl/src/mock_db/payment_attempt.rs @@ -235,6 +235,7 @@ impl PaymentAttemptInterface for MockDb { processor_merchant_id: payment_attempt.processor_merchant_id, created_by: payment_attempt.created_by, setup_future_usage_applied: payment_attempt.setup_future_usage_applied, + routing_approach: payment_attempt.routing_approach, }; payment_attempts.push(payment_attempt.clone()); Ok(payment_attempt) diff --git a/crates/storage_impl/src/payments/payment_attempt.rs b/crates/storage_impl/src/payments/payment_attempt.rs index 69de633aee..8eb8222afb 100644 --- a/crates/storage_impl/src/payments/payment_attempt.rs +++ b/crates/storage_impl/src/payments/payment_attempt.rs @@ -685,6 +685,7 @@ impl PaymentAttemptInterface for KVRouterStore { processor_merchant_id: payment_attempt.processor_merchant_id.clone(), created_by: payment_attempt.created_by.clone(), setup_future_usage_applied: payment_attempt.setup_future_usage_applied, + routing_approach: payment_attempt.routing_approach, }; let field = format!("pa_{}", created_attempt.attempt_id); @@ -1709,6 +1710,7 @@ impl DataModelExt for PaymentAttempt { issuer_error_code: self.issuer_error_code, issuer_error_message: self.issuer_error_message, setup_future_usage_applied: self.setup_future_usage_applied, + routing_approach: self.routing_approach, // Below fields are deprecated. Please add any new fields above this line. connector_transaction_data: None, processor_merchant_id: Some(self.processor_merchant_id), @@ -1803,6 +1805,7 @@ impl DataModelExt for PaymentAttempt { .created_by .and_then(|created_by| created_by.parse::().ok()), setup_future_usage_applied: storage_model.setup_future_usage_applied, + routing_approach: storage_model.routing_approach, } } } @@ -1892,6 +1895,7 @@ impl DataModelExt for PaymentAttemptNew { processor_merchant_id: Some(self.processor_merchant_id), created_by: self.created_by.map(|created_by| created_by.to_string()), setup_future_usage_applied: self.setup_future_usage_applied, + routing_approach: self.routing_approach, } } @@ -1974,6 +1978,7 @@ impl DataModelExt for PaymentAttemptNew { .created_by .and_then(|created_by| created_by.parse::().ok()), setup_future_usage_applied: storage_model.setup_future_usage_applied, + routing_approach: storage_model.routing_approach, } } } diff --git a/migrations/2025-06-19-124558_add_routing_approach_to_attempt/down.sql b/migrations/2025-06-19-124558_add_routing_approach_to_attempt/down.sql new file mode 100644 index 0000000000..0c6fce15b5 --- /dev/null +++ b/migrations/2025-06-19-124558_add_routing_approach_to_attempt/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` + +ALTER TABLE payment_attempt +DROP COLUMN IF EXISTS routing_approach; + +DROP TYPE "RoutingApproach"; diff --git a/migrations/2025-06-19-124558_add_routing_approach_to_attempt/up.sql b/migrations/2025-06-19-124558_add_routing_approach_to_attempt/up.sql new file mode 100644 index 0000000000..f5ec97a93f --- /dev/null +++ b/migrations/2025-06-19-124558_add_routing_approach_to_attempt/up.sql @@ -0,0 +1,15 @@ +-- Your SQL goes here +CREATE TYPE "RoutingApproach" AS ENUM ( + 'success_rate_exploitation', + 'success_rate_exploration', + 'contract_based_routing', + 'debit_routing', + 'rule_based_routing', + 'volume_based_routing', + 'default_fallback' +); + + +ALTER TABLE payment_attempt +ADD COLUMN IF NOT EXISTS routing_approach "RoutingApproach"; + diff --git a/v2_migrations/2025-01-13-081847_drop_v1_columns/up.sql b/v2_migrations/2025-01-13-081847_drop_v1_columns/up.sql index 7913af26d6..8eac6ae97a 100644 --- a/v2_migrations/2025-01-13-081847_drop_v1_columns/up.sql +++ b/v2_migrations/2025-01-13-081847_drop_v1_columns/up.sql @@ -93,7 +93,8 @@ ALTER TABLE payment_attempt DROP COLUMN attempt_id, DROP COLUMN charge_id, DROP COLUMN issuer_error_code, DROP COLUMN issuer_error_message, - DROP COLUMN setup_future_usage_applied; + DROP COLUMN setup_future_usage_applied, + DROP COLUMN routing_approach; ALTER TABLE payment_methods