Bug: Add support to call decision engine Dynamic Routing from hyperswitch after API key authentication




diff --git a/crates/api_models/src/events/routing.rs b/crates/api_models/src/events/routing.rs
index 62bc49dbe1b..9f09bac8274 100644
--- a/crates/api_models/src/events/routing.rs
+++ b/crates/api_models/src/events/routing.rs
@@ -178,3 +178,27 @@ impl ApiEventMetric for RuleMigrationError {
         Some(ApiEventsType::Routing)
     }
 }
+
+impl ApiEventMetric for crate::open_router::DecideGatewayResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Routing)
+    }
+}
+
+impl ApiEventMetric for crate::open_router::OpenRouterDecideGatewayRequest {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Routing)
+    }
+}
+
+impl ApiEventMetric for crate::open_router::UpdateScorePayload {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Routing)
+    }
+}
+
+impl ApiEventMetric for crate::open_router::UpdateScoreResponse {
+    fn get_api_event_type(&self) -> Option<ApiEventsType> {
+        Some(ApiEventsType::Routing)
+    }
+}
diff --git a/crates/api_models/src/open_router.rs b/crates/api_models/src/open_router.rs
index f5b80383bfb..aefbd3eb211 100644
--- a/crates/api_models/src/open_router.rs
+++ b/crates/api_models/src/open_router.rs
@@ -26,6 +26,41 @@ pub struct OpenRouterDecideGatewayRequest {
     pub elimination_enabled: Option<bool>,
 }
 
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct DecideGatewayResponse {
+    pub decided_gateway: Option<String>,
+    pub gateway_priority_map: Option<serde_json::Value>,
+    pub filter_wise_gateways: Option<serde_json::Value>,
+    pub priority_logic_tag: Option<String>,
+    pub routing_approach: Option<String>,
+    pub gateway_before_evaluation: Option<String>,
+    pub priority_logic_output: Option<PriorityLogicOutput>,
+    pub reset_approach: Option<String>,
+    pub routing_dimension: Option<String>,
+    pub routing_dimension_level: Option<String>,
+    pub is_scheduled_outage: Option<bool>,
+    pub is_dynamic_mga_enabled: Option<bool>,
+    pub gateway_mga_id_map: Option<serde_json::Value>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct PriorityLogicOutput {
+    pub is_enforcement: Option<bool>,
+    pub gws: Option<Vec<String>>,
+    pub priority_logic_tag: Option<String>,
+    pub gateway_reference_ids: Option<HashMap<String, String>>,
+    pub primary_logic: Option<PriorityLogicData>,
+    pub fallback_logic: Option<PriorityLogicData>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct PriorityLogicData {
+    pub name: Option<String>,
+    pub status: Option<String>,
+    pub failure_reason: Option<String>,
+}
+
 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
 #[serde(rename_all = "SCREAMING_SNAKE_CASE")]
 pub enum RankingAlgorithm {
diff --git a/crates/router/src/core/payments/routing/utils.rs b/crates/router/src/core/payments/routing/utils.rs
index d373b4188b1..5eb929a92d0 100644
--- a/crates/router/src/core/payments/routing/utils.rs
+++ b/crates/router/src/core/payments/routing/utils.rs
@@ -1232,7 +1232,7 @@ where
     String(String),
 }
 
-#[derive(Debug)]
+#[derive(Debug, Serialize)]
 pub struct RoutingEventsResponse<Res>
 where
     Res: Serialize + serde::de::DeserializeOwned + Clone,
diff --git a/crates/router/src/core/routing.rs b/crates/router/src/core/routing.rs
index 806a01235f3..2e6f8b09775 100644
--- a/crates/router/src/core/routing.rs
+++ b/crates/router/src/core/routing.rs
@@ -5,7 +5,12 @@ use std::collections::HashSet;
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
 use api_models::routing::DynamicRoutingAlgoAccessor;
 use api_models::{
-    enums, mandates as mandates_api, routing,
+    enums, mandates as mandates_api,
+    open_router::{
+        DecideGatewayResponse, OpenRouterDecideGatewayRequest, UpdateScorePayload,
+        UpdateScoreResponse,
+    },
+    routing,
     routing::{
         self as routing_types, RoutingRetrieveQuery, RuleMigrationError, RuleMigrationResponse,
     },
@@ -13,6 +18,7 @@ use api_models::{
 use async_trait::async_trait;
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
 use common_utils::ext_traits::AsyncExt;
+use common_utils::request::Method;
 use diesel_models::routing_algorithm::RoutingAlgorithm;
 use error_stack::ResultExt;
 #[cfg(all(feature = "v1", feature = "dynamic_routing"))]
@@ -2621,3 +2627,59 @@ pub async fn migrate_rules_for_profile(
         errors: error_list,
     })
 }
+
+pub async fn decide_gateway_open_router(
+    state: SessionState,
+    req_body: OpenRouterDecideGatewayRequest,
+) -> RouterResponse<DecideGatewayResponse> {
+    let response = if state.conf.open_router.dynamic_routing_enabled {
+        SRApiClient::send_decision_engine_request(
+            &state,
+            Method::Post,
+            "decide-gateway",
+            Some(req_body),
+            None,
+            None,
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)?
+        .response
+        .ok_or(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to perform decide gateway call with open router")?
+    } else {
+        Err(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Dynamic routing is not enabled")?
+    };
+
+    Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
+        response,
+    ))
+}
+
+pub async fn update_gateway_score_open_router(
+    state: SessionState,
+    req_body: UpdateScorePayload,
+) -> RouterResponse<UpdateScoreResponse> {
+    let response = if state.conf.open_router.dynamic_routing_enabled {
+        SRApiClient::send_decision_engine_request(
+            &state,
+            Method::Post,
+            "update-gateway-score",
+            Some(req_body),
+            None,
+            None,
+        )
+        .await
+        .change_context(errors::ApiErrorResponse::InternalServerError)?
+        .response
+        .ok_or(errors::ApiErrorResponse::InternalServerError)
+        .attach_printable("Failed to perform update gateway score call with open router")?
+    } else {
+        Err(errors::ApiErrorResponse::InternalServerError)
+            .attach_printable("Dynamic routing is not enabled")?
+    };
+
+    Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
+        response,
+    ))
+}
diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs
index cd132f0da63..e7127f43e87 100644
--- a/crates/router/src/routes/app.rs
+++ b/crates/router/src/routes/app.rs
@@ -973,6 +973,19 @@ impl Routing {
                 })),
             );
 
+        #[cfg(feature = "dynamic_routing")]
+        {
+            route = route
+                .service(
+                    web::resource("/evaluate")
+                        .route(web::post().to(routing::call_decide_gateway_open_router)),
+                )
+                .service(
+                    web::resource("/feedback")
+                        .route(web::post().to(routing::call_update_gateway_score_open_router)),
+                )
+        }
+
         #[cfg(feature = "payouts")]
         {
             route = route
diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs
index fda52ad9b0d..ab1e7bb1030 100644
--- a/crates/router/src/routes/lock_utils.rs
+++ b/crates/router/src/routes/lock_utils.rs
@@ -83,7 +83,9 @@ impl From<Flow> for ApiIdentifier {
             | Flow::DecisionManagerUpsertConfig
             | Flow::RoutingEvaluateRule
             | Flow::DecisionEngineRuleMigration
-            | Flow::VolumeSplitOnRoutingType => Self::Routing,
+            | Flow::VolumeSplitOnRoutingType
+            | Flow::DecisionEngineDecideGatewayCall
+            | Flow::DecisionEngineGatewayFeedbackCall => Self::Routing,
 
             Flow::RetrieveForexFlow => Self::Forex,
 
diff --git a/crates/router/src/routes/routing.rs b/crates/router/src/routes/routing.rs
index 024d4c0cfbe..ffd5c265db0 100644
--- a/crates/router/src/routes/routing.rs
+++ b/crates/router/src/routes/routing.rs
@@ -1672,3 +1672,51 @@ async fn get_merchant_account(
         .to_not_found_response(ApiErrorResponse::MerchantAccountNotFound)?;
     Ok((key_store, merchant_account))
 }
+
+#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))]
+#[instrument(skip_all)]
+pub async fn call_decide_gateway_open_router(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Json<api_models::open_router::OpenRouterDecideGatewayRequest>,
+) -> impl Responder {
+    let flow = Flow::DecisionEngineDecideGatewayCall;
+    Box::pin(oss_api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload.clone(),
+        |state, _auth, payload, _| routing::decide_gateway_open_router(state.clone(), payload),
+        &auth::ApiKeyAuth {
+            is_connected_allowed: false,
+            is_platform_allowed: false,
+        },
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
+
+#[cfg(all(feature = "olap", feature = "v1", feature = "dynamic_routing"))]
+#[instrument(skip_all)]
+pub async fn call_update_gateway_score_open_router(
+    state: web::Data<AppState>,
+    req: HttpRequest,
+    payload: web::Json<api_models::open_router::UpdateScorePayload>,
+) -> impl Responder {
+    let flow = Flow::DecisionEngineGatewayFeedbackCall;
+    Box::pin(oss_api::server_wrap(
+        flow,
+        state,
+        &req,
+        payload.clone(),
+        |state, _auth, payload, _| {
+            routing::update_gateway_score_open_router(state.clone(), payload)
+        },
+        &auth::ApiKeyAuth {
+            is_connected_allowed: false,
+            is_platform_allowed: false,
+        },
+        api_locking::LockAction::NotApplicable,
+    ))
+    .await
+}
diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs
index 155c5223375..d00d0f4e622 100644
--- a/crates/router_env/src/logger/types.rs
+++ b/crates/router_env/src/logger/types.rs
@@ -612,6 +612,10 @@ pub enum Flow {
     ThreeDsDecisionRuleExecute,
     /// Incoming Network Token Webhook Receive
     IncomingNetworkTokenWebhookReceive,
+    /// Decision Engine Decide Gateway Call
+    DecisionEngineDecideGatewayCall,
+    /// Decision Engine Gateway Feedback Call
+    DecisionEngineGatewayFeedbackCall,
 }
 
 /// Trait for providing generic behaviour to flow metric

## 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 API key auth to the decision engine endpoints for the decide gateway call and the update gateway score call.

### Additional Changes

- [x] 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).
-->
Closes #8613 

## 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)?
-->
<details>

<summary>decide-gateway</summary>

    curl --location 'http://localhost:8080/routing/evaluate' \
    --header 'Content-Type: application/json' \
    --header 'api-key: dev_UnrorzUwWmt9GUkRTalEmIjrWu3JwKNh8TkSZmMJ2Fsm***** \
    --data '{           
            "merchantId": "pro_VTQjooFPAyPOBC0aj9Jk",
            "eligibleGatewayList": ["GatewayA", "GatewayB", "GatewayC"],
            "rankingAlgorithm": "SR_BASED_ROUTING",
            "eliminationEnabled": true,
            "paymentInfo": {
                "paymentId": "PAY12359",
                "amount": 100,
                "currency": "USD",
                "customerId": "CUST12345",
                "udfs": null,
                "preferredGateway": null,
                "paymentType": "ORDER_PAYMENT",
                "metadata": null,
                "internalMetadata": null,
                "isEmi": false,
                "emiBank": null,
                "emiTenure": null,
                "paymentMethodType": "credit",
                "paymentMethod": "card",
                "paymentSource": null,
                "authType": null,
                "cardIssuerBankName": null,
                "cardIsin": null,
                "cardType": null,
                "cardSwitchProvider": null
            }
    }'

</details>
<details>
<summary>response</summary>

    {
        "decided_gateway": "GatewayA",
        "gateway_priority_map": {
            "GatewayA": 1.0,
            "GatewayB": 1.0,
            "GatewayC": 1.0
        },
        "filter_wise_gateways": null,
        "priority_logic_tag": null,
        "routing_approach": "SR_SELECTION_V3_ROUTING",
        "gateway_before_evaluation": "GatewayC",
        "priority_logic_output": {
            "isEnforcement": false,
            "gws": [
                "GatewayA",
                "GatewayB",
                "GatewayC"
            ],
            "priorityLogicTag": null,
            "gatewayReferenceIds": {},
            "primaryLogic": null,
            "fallbackLogic": null
        },
        "reset_approach": "NO_RESET",
        "routing_dimension": "ORDER_PAYMENT, CREDIT, card",
        "routing_dimension_level": "PM_LEVEL",
        "is_scheduled_outage": false,
        "is_dynamic_mga_enabled": false,
        "gateway_mga_id_map": null
    }

</details>

---

<details>

<summary>update-gateway-score</summary>

    curl --location 'http://localhost:8080/routing/feedback' \
    --header 'Content-Type: application/json' \
    --header 'api-key: dev_UnrorzUwWmt9GUkRTalEmIjrWu3JwKNh8TkSZmMJ2Fs******' \
    --data '{
      "merchantId" : "pro_VTQjooFPAyPOBC0aj9Jk",
      "gateway": "GatewayC",
      "gatewayReferenceId": null,
      "status": "FAILURE",
      "paymentId": "PAY12359",
      "enforceDynamicRoutingFailure" : null
    }'

</details>
<details>
<summary>response</summary>

    {
        "message": "Success"
    }

</details>

## 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
