Bug: open router integration of elimination routing



Add support for integrating open router's elimination routing in Hyperswitch
diff --git a/crates/api_models/src/open_router.rs b/crates/api_models/src/open_router.rs
index c33365df169..07af605892f 100644
--- a/crates/api_models/src/open_router.rs
+++ b/crates/api_models/src/open_router.rs
@@ -10,14 +10,14 @@ pub use euclid::{
 };
 use serde::{Deserialize, Serialize};
 
-use crate::enums::{Currency, PaymentMethod, RoutableConnectors};
+use crate::enums::{Currency, PaymentMethod};
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 #[serde(rename_all = "camelCase")]
 pub struct OpenRouterDecideGatewayRequest {
     pub payment_info: PaymentInfo,
     pub merchant_id: id_type::ProfileId,
-    pub eligible_gateway_list: Option<Vec<RoutableConnectors>>,
+    pub eligible_gateway_list: Option<Vec<String>>,
     pub ranking_algorithm: Option<RankingAlgorithm>,
     pub elimination_enabled: Option<bool>,
 }
@@ -80,7 +80,7 @@ pub struct UnifiedError {
 #[serde(rename_all = "camelCase")]
 pub struct UpdateScorePayload {
     pub merchant_id: id_type::ProfileId,
-    pub gateway: RoutableConnectors,
+    pub gateway: String,
     pub status: TxnStatus,
     pub payment_id: id_type::PaymentId,
 }
@@ -91,7 +91,7 @@ pub enum TxnStatus {
     Started,
     AuthenticationFailed,
     JuspayDeclined,
-    PendingVBV,
+    PendingVbv,
     VBVSuccessful,
     Authorized,
     AuthorizationFailed,
@@ -111,12 +111,3 @@ pub enum TxnStatus {
     Failure,
     Declined,
 }
-
-impl From<bool> for TxnStatus {
-    fn from(value: bool) -> Self {
-        match value {
-            true => Self::Charged,
-            _ => Self::Failure,
-        }
-    }
-}
diff --git a/crates/api_models/src/routing.rs b/crates/api_models/src/routing.rs
index d3529edd29e..a683e3343aa 100644
--- a/crates/api_models/src/routing.rs
+++ b/crates/api_models/src/routing.rs
@@ -642,6 +642,17 @@ impl DynamicRoutingAlgorithmRef {
     pub fn update_volume_split(&mut self, volume: Option<u8>) {
         self.dynamic_routing_volume_split = volume
     }
+
+    pub fn is_elimination_enabled(&self) -> bool {
+        self.elimination_routing_algorithm
+            .as_ref()
+            .map(|elimination_routing| {
+                elimination_routing.enabled_feature
+                    == DynamicRoutingFeatures::DynamicConnectorSelection
+                    || elimination_routing.enabled_feature == DynamicRoutingFeatures::Metrics
+            })
+            .unwrap_or_default()
+    }
 }
 
 #[derive(Debug, Default, Clone, Copy, serde::Serialize, serde::Deserialize)]
diff --git a/crates/router/src/core/payments/routing.rs b/crates/router/src/core/payments/routing.rs
index 038751451ac..6bc55203615 100644
--- a/crates/router/src/core/payments/routing.rs
+++ b/crates/router/src/core/payments/routing.rs
@@ -1516,6 +1516,7 @@ pub async fn perform_open_routing(
         profile.get_id().get_string_repr()
     );
 
+    let is_elimination_enabled = dynamic_routing_algo_ref.is_elimination_enabled();
     let connectors = dynamic_routing_algo_ref
         .success_based_algorithm
         .async_map(|algo| {
@@ -1524,16 +1525,34 @@ pub async fn perform_open_routing(
                 routable_connectors.clone(),
                 profile.get_id(),
                 algo,
-                payment_data,
+                &payment_data,
+                is_elimination_enabled,
             )
         })
         .await
-        .transpose()
-        .inspect_err(|e| logger::error!(dynamic_routing_error=?e))
-        .ok()
-        .flatten()
+        .transpose()?
         .unwrap_or(routable_connectors);
 
+    if is_elimination_enabled {
+        // This will initiate the elimination process for the connector.
+        // Penalize the elimination score of the connector before making a payment.
+        // Once the payment is made, we will update the score based on the payment status
+        if let Some(connector) = connectors.first() {
+            logger::debug!(
+                "penalizing the elimination score of the gateway with id {} in open router for profile {}",
+                connector, profile.get_id().get_string_repr()
+            );
+            update_success_rate_score_with_open_router(
+                state,
+                connector.clone(),
+                profile.get_id(),
+                &payment_data.payment_id,
+                common_enums::AttemptStatus::AuthenticationPending,
+            )
+            .await?
+        }
+    }
+
     Ok(connectors)
 }
 
@@ -1634,7 +1653,8 @@ pub async fn perform_success_based_routing_with_open_router(
     mut routable_connectors: Vec<api_routing::RoutableConnectorChoice>,
     profile_id: &common_utils::id_type::ProfileId,
     success_based_algo_ref: api_routing::SuccessBasedAlgorithm,
-    payment_attempt: oss_storage::PaymentAttempt,
+    payment_attempt: &oss_storage::PaymentAttempt,
+    is_elimination_enabled: bool,
 ) -> RoutingResult<Vec<api_routing::RoutableConnectorChoice>> {
     if success_based_algo_ref.enabled_feature
         == api_routing::DynamicRoutingFeatures::DynamicConnectorSelection
@@ -1646,11 +1666,9 @@ pub async fn perform_success_based_routing_with_open_router(
 
         let open_router_req_body = OpenRouterDecideGatewayRequest::construct_sr_request(
             payment_attempt,
-            routable_connectors
-                .iter()
-                .map(|gateway| gateway.connector)
-                .collect::<Vec<_>>(),
+            routable_connectors.clone(),
             Some(or_types::RankingAlgorithm::SrBasedRouting),
+            is_elimination_enabled,
         );
 
         let url = format!("{}/{}", &state.conf.open_router.url, "decide-gateway");
@@ -1724,15 +1742,15 @@ pub async fn perform_success_based_routing_with_open_router(
 #[instrument(skip_all)]
 pub async fn update_success_rate_score_with_open_router(
     state: &SessionState,
-    payment_connector: common_enums::RoutableConnectors,
+    payment_connector: api_routing::RoutableConnectorChoice,
     profile_id: &common_utils::id_type::ProfileId,
     payment_id: &common_utils::id_type::PaymentId,
-    payment_status: bool,
+    payment_status: common_enums::AttemptStatus,
 ) -> RoutingResult<()> {
     let open_router_req_body = or_types::UpdateScorePayload {
         merchant_id: profile_id.clone(),
-        gateway: payment_connector,
-        status: payment_status.into(),
+        gateway: payment_connector.to_string(),
+        status: payment_status.foreign_into(),
         payment_id: payment_id.clone(),
     };
 
@@ -1763,7 +1781,8 @@ pub async fn update_success_rate_score_with_open_router(
             )?;
 
             logger::debug!(
-                "Open router update_gateway_score response: {:?}",
+                "Open router update_gateway_score response for gateway with id {}: {:?}",
+                payment_connector,
                 update_score_resp
             );
 
diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs
index b10500d2844..cf14c70ca32 100644
--- a/crates/router/src/core/routing/helpers.rs
+++ b/crates/router/src/core/routing/helpers.rs
@@ -8,6 +8,8 @@ use std::str::FromStr;
 #[cfg(all(feature = "dynamic_routing", feature = "v1"))]
 use std::sync::Arc;
 
+#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
+use api_models::open_router;
 use api_models::routing as routing_types;
 #[cfg(all(feature = "dynamic_routing", feature = "v1"))]
 use common_utils::ext_traits::ValueExt;
@@ -39,6 +41,8 @@ use storage_impl::redis::cache::Cacheable;
 use crate::db::errors::StorageErrorExt;
 #[cfg(feature = "v2")]
 use crate::types::domain::MerchantConnectorAccount;
+#[cfg(all(feature = "dynamic_routing", feature = "v1"))]
+use crate::types::transformers::ForeignFrom;
 use crate::{
     core::errors::{self, RouterResult},
     db::StorageInterface,
@@ -729,20 +733,27 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
                 },
             )?;
 
-            let payment_status_attribute =
-                get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status);
+            let routable_connector = routing_types::RoutableConnectorChoice {
+                choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
+                connector: common_enums::RoutableConnectors::from_str(payment_connector.as_str())
+                    .change_context(errors::ApiErrorResponse::InternalServerError)
+                    .attach_printable("unable to infer routable_connector from connector")?,
+                merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
+            };
 
             let should_route_to_open_router = state.conf.open_router.enabled;
 
             if should_route_to_open_router {
+                logger::debug!(
+                    "performing update-gateway-score for gateway with id {} in open router for profile: {}",
+                    routable_connector, profile_id.get_string_repr()
+                );
                 routing::payments_routing::update_success_rate_score_with_open_router(
                     state,
-                    common_enums::RoutableConnectors::from_str(payment_connector.as_str())
-                        .change_context(errors::ApiErrorResponse::InternalServerError)
-                        .attach_printable("unable to infer routable_connector from connector")?,
+                    routable_connector.clone(),
                     profile_id,
                     &payment_attempt.payment_id,
-                    payment_status_attribute == common_enums::AttemptStatus::Charged,
+                    payment_attempt.status,
                 )
                 .await
                 .change_context(errors::ApiErrorResponse::InternalServerError)
@@ -751,6 +762,9 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
                 return Ok(());
             }
 
+            let payment_status_attribute =
+                get_desired_payment_status_for_dynamic_routing_metrics(payment_attempt.status);
+
             let success_based_routing_configs = fetch_dynamic_routing_configs::<
                 routing_types::SuccessBasedRoutingConfig,
             >(
@@ -972,17 +986,7 @@ pub async fn push_metrics_with_update_window_for_success_based_routing(
                     success_based_routing_configs,
                     success_based_routing_config_params,
                     vec![routing_types::RoutableConnectorChoiceWithStatus::new(
-                        routing_types::RoutableConnectorChoice {
-                            choice_kind: api_models::routing::RoutableChoiceKind::FullStruct,
-                            connector: common_enums::RoutableConnectors::from_str(
-                                payment_connector.as_str(),
-                            )
-                            .change_context(errors::ApiErrorResponse::InternalServerError)
-                            .attach_printable(
-                                "unable to infer routable_connector from connector",
-                            )?,
-                            merchant_connector_id: payment_attempt.merchant_connector_id.clone(),
-                        },
+                        routable_connector,
                         payment_status_attribute == common_enums::AttemptStatus::Charged,
                     )],
                     state.get_grpc_headers(),
@@ -1350,6 +1354,38 @@ fn get_desired_payment_status_for_dynamic_routing_metrics(
     }
 }
 
+#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
+impl ForeignFrom<common_enums::AttemptStatus> for open_router::TxnStatus {
+    fn foreign_from(attempt_status: common_enums::AttemptStatus) -> Self {
+        match attempt_status {
+            common_enums::AttemptStatus::Started => Self::Started,
+            common_enums::AttemptStatus::AuthenticationFailed => Self::AuthenticationFailed,
+            common_enums::AttemptStatus::RouterDeclined => Self::JuspayDeclined,
+            common_enums::AttemptStatus::AuthenticationPending => Self::PendingVbv,
+            common_enums::AttemptStatus::AuthenticationSuccessful => Self::VBVSuccessful,
+            common_enums::AttemptStatus::Authorized => Self::Authorized,
+            common_enums::AttemptStatus::AuthorizationFailed => Self::AuthorizationFailed,
+            common_enums::AttemptStatus::Charged => Self::Charged,
+            common_enums::AttemptStatus::Authorizing => Self::Authorizing,
+            common_enums::AttemptStatus::CodInitiated => Self::CODInitiated,
+            common_enums::AttemptStatus::Voided => Self::Voided,
+            common_enums::AttemptStatus::VoidInitiated => Self::VoidInitiated,
+            common_enums::AttemptStatus::CaptureInitiated => Self::CaptureInitiated,
+            common_enums::AttemptStatus::CaptureFailed => Self::CaptureFailed,
+            common_enums::AttemptStatus::VoidFailed => Self::VoidFailed,
+            common_enums::AttemptStatus::AutoRefunded => Self::AutoRefunded,
+            common_enums::AttemptStatus::PartialCharged => Self::PartialCharged,
+            common_enums::AttemptStatus::PartialChargedAndChargeable => Self::ToBeCharged,
+            common_enums::AttemptStatus::Unresolved => Self::Pending,
+            common_enums::AttemptStatus::Pending => Self::Pending,
+            common_enums::AttemptStatus::Failure => Self::Failure,
+            common_enums::AttemptStatus::PaymentMethodAwaited => Self::Pending,
+            common_enums::AttemptStatus::ConfirmationAwaited => Self::Pending,
+            common_enums::AttemptStatus::DeviceDataCollectionPending => Self::Pending,
+        }
+    }
+}
+
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
 fn get_dynamic_routing_based_metrics_outcome_for_payment(
     payment_status_attribute: common_enums::AttemptStatus,
diff --git a/crates/router/src/core/routing/transformers.rs b/crates/router/src/core/routing/transformers.rs
index d73e0d28671..85b6e5832a8 100644
--- a/crates/router/src/core/routing/transformers.rs
+++ b/crates/router/src/core/routing/transformers.rs
@@ -1,11 +1,12 @@
-#[cfg(all(feature = "v1", feature = "dynamic_routing"))]
-use api_models::open_router::{OpenRouterDecideGatewayRequest, PaymentInfo, RankingAlgorithm};
 use api_models::routing::{
     MerchantRoutingAlgorithm, RoutingAlgorithm as Algorithm, RoutingAlgorithmKind,
     RoutingDictionaryRecord,
 };
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
-use common_enums::RoutableConnectors;
+use api_models::{
+    open_router::{OpenRouterDecideGatewayRequest, PaymentInfo, RankingAlgorithm},
+    routing::RoutableConnectorChoice,
+};
 use common_utils::ext_traits::ValueExt;
 use diesel_models::{
     enums as storage_enums,
@@ -108,9 +109,10 @@ impl From<&routing::TransactionData<'_>> for storage_enums::TransactionType {
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
 pub trait OpenRouterDecideGatewayRequestExt {
     fn construct_sr_request(
-        attempt: PaymentAttempt,
-        eligible_gateway_list: Vec<RoutableConnectors>,
+        attempt: &PaymentAttempt,
+        eligible_gateway_list: Vec<RoutableConnectorChoice>,
         ranking_algorithm: Option<RankingAlgorithm>,
+        is_elimination_enabled: bool,
     ) -> Self
     where
         Self: Sized;
@@ -119,13 +121,14 @@ pub trait OpenRouterDecideGatewayRequestExt {
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
 impl OpenRouterDecideGatewayRequestExt for OpenRouterDecideGatewayRequest {
     fn construct_sr_request(
-        attempt: PaymentAttempt,
-        eligible_gateway_list: Vec<RoutableConnectors>,
+        attempt: &PaymentAttempt,
+        eligible_gateway_list: Vec<RoutableConnectorChoice>,
         ranking_algorithm: Option<RankingAlgorithm>,
+        is_elimination_enabled: bool,
     ) -> Self {
         Self {
             payment_info: PaymentInfo {
-                payment_id: attempt.payment_id,
+                payment_id: attempt.payment_id.clone(),
                 amount: attempt.net_amount.get_order_amount(),
                 currency: attempt.currency.unwrap_or(storage_enums::Currency::USD),
                 payment_type: "ORDER_PAYMENT".to_string(),
@@ -133,10 +136,15 @@ impl OpenRouterDecideGatewayRequestExt for OpenRouterDecideGatewayRequest {
                 payment_method_type: "UPI".into(), // TODO: once open-router makes this field string, we can send from attempt
                 payment_method: attempt.payment_method.unwrap_or_default(),
             },
-            merchant_id: attempt.profile_id,
-            eligible_gateway_list: Some(eligible_gateway_list),
+            merchant_id: attempt.profile_id.clone(),
+            eligible_gateway_list: Some(
+                eligible_gateway_list
+                    .into_iter()
+                    .map(|connector| connector.to_string())
+                    .collect(),
+            ),
             ranking_algorithm,
-            elimination_enabled: None,
+            elimination_enabled: Some(is_elimination_enabled),
         }
     }
 }

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [ ] Bugfix
- [x] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
This PR adds the elimination routing integration support with open router.

### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->


## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Create merchant_account, api_key, mca 
1. Enable elimination routing for merchant
```
curl --location --request POST 'http://localhost:8080/account/merchant_1744368166/business_profile/pro_GrzjTHd5Pn5hAhwTZPRn/dynamic_routing/elimination/toggle?enable=dynamic_connector_selection' \
--header 'api-key: xyz'
```
2. Enable Volume split
```
curl --location --request POST 'http://localhost:8080/account/merchant_1744368166/business_profile/pro_GrzjTHd5Pn5hAhwTZPRn/dynamic_routing/set_volume_split?split=100' \
--header 'api-key: xyz'
```
3. Create a Payment
Search for the log " penalizing the elimination score of the gateway with id" 
![image](https://github.com/user-attachments/assets/16bd41b3-5dd9-4911-b64e-4f53ceb50689)


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
