Bug: Add trait based implementation for relay



Refactor the relay flow to add trait based implication for it
diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs
index f54e1471632..aded73b4345 100644
--- a/crates/api_models/src/relay.rs
+++ b/crates/api_models/src/relay.rs
@@ -24,11 +24,11 @@ pub struct RelayRequest {
 #[serde(rename_all = "snake_case")]
 pub enum RelayData {
     /// The data that is associated with a refund relay request
-    Refund(RelayRefundRequest),
+    Refund(RelayRefundRequestData),
 }
 
 #[derive(Debug, ToSchema, Clone, Deserialize, Serialize)]
-pub struct RelayRefundRequest {
+pub struct RelayRefundRequestData {
     /// The amount that is being refunded
     #[schema(value_type = i64 , example = 6540)]
     pub amount: MinorUnit,
diff --git a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs
index 9208bb486f7..1e602c02824 100644
--- a/crates/hyperswitch_domain_models/src/merchant_connector_account.rs
+++ b/crates/hyperswitch_domain_models/src/merchant_connector_account.rs
@@ -87,6 +87,10 @@ impl MerchantConnectorAccount {
     pub fn get_connector_test_mode(&self) -> Option<bool> {
         self.test_mode
     }
+
+    pub fn get_connector_name_as_string(&self) -> String {
+        self.connector_name.clone()
+    }
 }
 
 #[cfg(feature = "v2")]
@@ -151,6 +155,10 @@ impl MerchantConnectorAccount {
     pub fn get_connector_test_mode(&self) -> Option<bool> {
         todo!()
     }
+
+    pub fn get_connector_name_as_string(&self) -> String {
+        self.connector_name.clone().to_string()
+    }
 }
 
 #[cfg(feature = "v2")]
diff --git a/crates/hyperswitch_domain_models/src/relay.rs b/crates/hyperswitch_domain_models/src/relay.rs
index 8af58265c39..d4829c7599e 100644
--- a/crates/hyperswitch_domain_models/src/relay.rs
+++ b/crates/hyperswitch_domain_models/src/relay.rs
@@ -74,6 +74,16 @@ impl From<api_models::relay::RelayData> for RelayData {
     }
 }
 
+impl From<api_models::relay::RelayRefundRequestData> for RelayRefundData {
+    fn from(relay: api_models::relay::RelayRefundRequestData) -> Self {
+        Self {
+            amount: relay.amount,
+            currency: relay.currency,
+            reason: relay.reason,
+        }
+    }
+}
+
 impl RelayUpdate {
     pub fn from(
         response: Result<router_response_types::RefundsResponseData, ErrorResponse>,
@@ -92,6 +102,20 @@ impl RelayUpdate {
     }
 }
 
+impl From<RelayData> for api_models::relay::RelayData {
+    fn from(relay: RelayData) -> Self {
+        match relay {
+            RelayData::Refund(relay_refund_request) => {
+                Self::Refund(api_models::relay::RelayRefundRequestData {
+                    amount: relay_refund_request.amount,
+                    currency: relay_refund_request.currency,
+                    reason: relay_refund_request.reason,
+                })
+            }
+        }
+    }
+}
+
 impl From<Relay> for api_models::relay::RelayResponse {
     fn from(value: Relay) -> Self {
         let error = value
@@ -106,7 +130,7 @@ impl From<Relay> for api_models::relay::RelayResponse {
 
         let data = value.request_data.map(|relay_data| match relay_data {
             RelayData::Refund(relay_refund_request) => {
-                api_models::relay::RelayData::Refund(api_models::relay::RelayRefundRequest {
+                api_models::relay::RelayData::Refund(api_models::relay::RelayRefundRequestData {
                     amount: relay_refund_request.amount,
                     currency: relay_refund_request.currency,
                     reason: relay_refund_request.reason,
diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs
index eb5edb08fab..06931f693d7 100644
--- a/crates/openapi/src/openapi.rs
+++ b/crates/openapi/src/openapi.rs
@@ -537,7 +537,7 @@ Never share your secret api keys. Keep them guarded and secure.
         api_models::relay::RelayResponse,
         api_models::enums::RelayType,
         api_models::relay::RelayData,
-        api_models::relay::RelayRefundRequest,
+        api_models::relay::RelayRefundRequestData,
         api_models::enums::RelayStatus,
         api_models::relay::RelayError,
         api_models::payments::AmountFilter,
diff --git a/crates/router/src/core/relay.rs b/crates/router/src/core/relay.rs
index 9492816452a..474c371b7cc 100644
--- a/crates/router/src/core/relay.rs
+++ b/crates/router/src/core/relay.rs
@@ -1,7 +1,14 @@
-use api_models::relay as relay_models;
+use std::marker::PhantomData;
+
+use api_models::relay as relay_api_models;
+use async_trait::async_trait;
 use common_enums::RelayStatus;
-use common_utils::{self, id_type};
+use common_utils::{
+    self, fp_utils,
+    id_type::{self, GenerateId},
+};
 use error_stack::ResultExt;
+use hyperswitch_domain_models::relay;
 
 use super::errors::{self, ConnectorErrorExt, RouterResponse, RouterResult, StorageErrorExt};
 use crate::{
@@ -17,13 +24,208 @@ use crate::{
 
 pub mod utils;
 
-pub async fn relay(
+pub trait Validate {
+    type Error: error_stack::Context;
+    fn validate(&self) -> Result<(), Self::Error>;
+}
+
+impl Validate for relay_api_models::RelayRefundRequestData {
+    type Error = errors::ApiErrorResponse;
+    fn validate(&self) -> Result<(), Self::Error> {
+        fp_utils::when(self.amount.get_amount_as_i64() <= 0, || {
+            Err(errors::ApiErrorResponse::PreconditionFailed {
+                message: "Amount should be greater than 0".to_string(),
+            })
+        })?;
+        Ok(())
+    }
+}
+
+#[async_trait]
+pub trait RelayInterface {
+    type Request: Validate;
+    fn validate_relay_request(req: &Self::Request) -> RouterResult<()> {
+        req.validate()
+            .change_context(errors::ApiErrorResponse::PreconditionFailed {
+                message: "Invalid relay request".to_string(),
+            })
+    }
+
+    fn get_domain_models(
+        relay_request: RelayRequestInner<Self>,
+        merchant_id: &id_type::MerchantId,
+        profile_id: &id_type::ProfileId,
+    ) -> relay::Relay;
+
+    async fn process_relay(
+        state: &SessionState,
+        merchant_account: domain::MerchantAccount,
+        connector_account: domain::MerchantConnectorAccount,
+        relay_record: &relay::Relay,
+    ) -> RouterResult<relay::RelayUpdate>;
+
+    fn generate_response(value: relay::Relay) -> RouterResult<api_models::relay::RelayResponse>;
+}
+
+pub struct RelayRequestInner<T: RelayInterface + ?Sized> {
+    pub connector_resource_id: String,
+    pub connector_id: id_type::MerchantConnectorAccountId,
+    pub relay_type: PhantomData<T>,
+    pub data: T::Request,
+}
+
+impl RelayRequestInner<RelayRefund> {
+    pub fn from_relay_request(relay_request: relay_api_models::RelayRequest) -> RouterResult<Self> {
+        match relay_request.data {
+            Some(relay_api_models::RelayData::Refund(ref_data)) => Ok(Self {
+                connector_resource_id: relay_request.connector_resource_id,
+                connector_id: relay_request.connector_id,
+                relay_type: PhantomData,
+                data: ref_data,
+            }),
+            None => Err(errors::ApiErrorResponse::InvalidRequestData {
+                message: "Relay data is required for relay type refund".to_string(),
+            })?,
+        }
+    }
+}
+
+pub struct RelayRefund;
+
+#[async_trait]
+impl RelayInterface for RelayRefund {
+    type Request = relay_api_models::RelayRefundRequestData;
+
+    fn get_domain_models(
+        relay_request: RelayRequestInner<Self>,
+        merchant_id: &id_type::MerchantId,
+        profile_id: &id_type::ProfileId,
+    ) -> relay::Relay {
+        let relay_id = id_type::RelayId::generate();
+        let relay_refund: relay::RelayRefundData = relay_request.data.into();
+        relay::Relay {
+            id: relay_id.clone(),
+            connector_resource_id: relay_request.connector_resource_id.clone(),
+            connector_id: relay_request.connector_id.clone(),
+            profile_id: profile_id.clone(),
+            merchant_id: merchant_id.clone(),
+            relay_type: common_enums::RelayType::Refund,
+            request_data: Some(relay::RelayData::Refund(relay_refund)),
+            status: RelayStatus::Created,
+            connector_reference_id: None,
+            error_code: None,
+            error_message: None,
+            created_at: common_utils::date_time::now(),
+            modified_at: common_utils::date_time::now(),
+            response_data: None,
+        }
+    }
+
+    async fn process_relay(
+        state: &SessionState,
+        merchant_account: domain::MerchantAccount,
+        connector_account: domain::MerchantConnectorAccount,
+        relay_record: &relay::Relay,
+    ) -> RouterResult<relay::RelayUpdate> {
+        let connector_id = &relay_record.connector_id;
+
+        let merchant_id = merchant_account.get_id();
+
+        let connector_name = &connector_account.get_connector_name_as_string();
+
+        let connector_data = api::ConnectorData::get_connector_by_name(
+            &state.conf.connectors,
+            connector_name,
+            api::GetToken::Connector,
+            Some(connector_id.clone()),
+        )?;
+
+        let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
+            api::Execute,
+            hyperswitch_domain_models::router_request_types::RefundsData,
+            hyperswitch_domain_models::router_response_types::RefundsResponseData,
+        > = connector_data.connector.get_connector_integration();
+
+        let router_data = utils::construct_relay_refund_router_data(
+            state,
+            merchant_id,
+            &connector_account,
+            relay_record,
+        )
+        .await?;
+
+        let router_data_res = services::execute_connector_processing_step(
+            state,
+            connector_integration,
+            &router_data,
+            payments::CallConnectorAction::Trigger,
+            None,
+        )
+        .await
+        .to_refund_failed_response()?;
+
+        let relay_update = relay::RelayUpdate::from(router_data_res.response);
+
+        Ok(relay_update)
+    }
+
+    fn generate_response(value: relay::Relay) -> RouterResult<api_models::relay::RelayResponse> {
+        let error = value
+            .error_code
+            .zip(value.error_message)
+            .map(
+                |(error_code, error_message)| api_models::relay::RelayError {
+                    code: error_code,
+                    message: error_message,
+                },
+            );
+
+        let data =
+            api_models::relay::RelayData::from(value.request_data.get_required_value("RelayData")?);
+
+        Ok(api_models::relay::RelayResponse {
+            id: value.id,
+            status: value.status,
+            error,
+            connector_resource_id: value.connector_resource_id,
+            connector_id: value.connector_id,
+            profile_id: value.profile_id,
+            relay_type: value.relay_type,
+            data: Some(data),
+            connector_reference_id: value.connector_reference_id,
+        })
+    }
+}
+
+pub async fn relay_flow_decider(
     state: SessionState,
     merchant_account: domain::MerchantAccount,
     profile_id_optional: Option<id_type::ProfileId>,
     key_store: domain::MerchantKeyStore,
-    req: relay_models::RelayRequest,
-) -> RouterResponse<relay_models::RelayResponse> {
+    request: relay_api_models::RelayRequest,
+) -> RouterResponse<relay_api_models::RelayResponse> {
+    let relay_flow_request = match request.relay_type {
+        common_enums::RelayType::Refund => {
+            RelayRequestInner::<RelayRefund>::from_relay_request(request)?
+        }
+    };
+    relay(
+        state,
+        merchant_account,
+        profile_id_optional,
+        key_store,
+        relay_flow_request,
+    )
+    .await
+}
+
+pub async fn relay<T: RelayInterface>(
+    state: SessionState,
+    merchant_account: domain::MerchantAccount,
+    profile_id_optional: Option<id_type::ProfileId>,
+    key_store: domain::MerchantKeyStore,
+    req: RelayRequestInner<T>,
+) -> RouterResponse<relay_api_models::RelayResponse> {
     let db = state.store.as_ref();
     let key_manager_state = &(&state).into();
     let merchant_id = merchant_account.get_id();
@@ -64,10 +266,9 @@ pub async fn relay(
             id: connector_id.get_string_repr().to_string(),
         })?;
 
-    validate_relay_refund_request(&req).attach_printable("Invalid relay refund request")?;
+    T::validate_relay_request(&req.data)?;
 
-    let relay_domain =
-        hyperswitch_domain_models::relay::Relay::new(&req, merchant_id, profile.get_id());
+    let relay_domain = T::get_domain_models(req, merchant_id, profile.get_id());
 
     let relay_record = db
         .insert_relay(key_manager_state, &key_store, relay_domain)
@@ -75,117 +276,31 @@ pub async fn relay(
         .change_context(errors::ApiErrorResponse::InternalServerError)
         .attach_printable("Failed to insert a relay record in db")?;
 
-    let relay_response = match req.relay_type {
-        common_enums::RelayType::Refund => {
-            Box::pin(relay_refund(
-                &state,
-                merchant_account,
-                connector_account,
-                &relay_record,
-            ))
-            .await?
-        }
-    };
+    let relay_response =
+        T::process_relay(&state, merchant_account, connector_account, &relay_record)
+            .await
+            .attach_printable("Failed to process relay")?;
 
     let relay_update_record = db
         .update_relay(key_manager_state, &key_store, relay_record, relay_response)
         .await
         .change_context(errors::ApiErrorResponse::InternalServerError)?;
 
-    let response = relay_models::RelayResponse::from(relay_update_record);
+    let response = T::generate_response(relay_update_record)
+        .attach_printable("Failed to generate relay response")?;
 
     Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
         response,
     ))
 }
 
-pub async fn relay_refund(
-    state: &SessionState,
-    merchant_account: domain::MerchantAccount,
-    connector_account: domain::MerchantConnectorAccount,
-    relay_record: &hyperswitch_domain_models::relay::Relay,
-) -> RouterResult<hyperswitch_domain_models::relay::RelayUpdate> {
-    let connector_id = &relay_record.connector_id;
-
-    let merchant_id = merchant_account.get_id();
-
-    #[cfg(feature = "v1")]
-    let connector_name = &connector_account.connector_name;
-
-    #[cfg(feature = "v2")]
-    let connector_name = &connector_account.connector_name.to_string();
-
-    let connector_data = api::ConnectorData::get_connector_by_name(
-        &state.conf.connectors,
-        connector_name,
-        api::GetToken::Connector,
-        Some(connector_id.clone()),
-    )?;
-
-    let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
-        api::Execute,
-        hyperswitch_domain_models::router_request_types::RefundsData,
-        hyperswitch_domain_models::router_response_types::RefundsResponseData,
-    > = connector_data.connector.get_connector_integration();
-
-    let router_data = utils::construct_relay_refund_router_data(
-        state,
-        merchant_id,
-        &connector_account,
-        relay_record,
-    )
-    .await?;
-
-    let router_data_res = services::execute_connector_processing_step(
-        state,
-        connector_integration,
-        &router_data,
-        payments::CallConnectorAction::Trigger,
-        None,
-    )
-    .await
-    .to_refund_failed_response()?;
-
-    let relay_response =
-        hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response);
-
-    Ok(relay_response)
-}
-
-// validate relay request
-pub fn validate_relay_refund_request(
-    relay_request: &relay_models::RelayRequest,
-) -> RouterResult<()> {
-    match (relay_request.relay_type, &relay_request.data) {
-        (common_enums::RelayType::Refund, Some(relay_models::RelayData::Refund(ref_data))) => {
-            validate_relay_refund_data(ref_data)
-        }
-        (common_enums::RelayType::Refund, None) => {
-            Err(errors::ApiErrorResponse::PreconditionFailed {
-                message: "Relay data is required for refund relay".to_string(),
-            })?
-        }
-    }
-}
-
-pub fn validate_relay_refund_data(
-    refund_data: &relay_models::RelayRefundRequest,
-) -> RouterResult<()> {
-    if refund_data.amount.get_amount_as_i64() <= 0 {
-        Err(errors::ApiErrorResponse::PreconditionFailed {
-            message: "Amount should be greater than 0".to_string(),
-        })?
-    }
-    Ok(())
-}
-
 pub async fn relay_retrieve(
     state: SessionState,
     merchant_account: domain::MerchantAccount,
     profile_id_optional: Option<id_type::ProfileId>,
     key_store: domain::MerchantKeyStore,
-    req: relay_models::RelayRetrieveRequest,
-) -> RouterResponse<relay_models::RelayResponse> {
+    req: relay_api_models::RelayRetrieveRequest,
+) -> RouterResponse<relay_api_models::RelayResponse> {
     let db = state.store.as_ref();
     let key_manager_state = &(&state).into();
     let merchant_id = merchant_account.get_id();
@@ -269,17 +384,14 @@ pub async fn relay_retrieve(
         }
     };
 
-    let response = relay_models::RelayResponse::from(relay_response);
+    let response = relay_api_models::RelayResponse::from(relay_response);
 
     Ok(hyperswitch_domain_models::api::ApplicationResponse::Json(
         response,
     ))
 }
 
-fn should_call_connector_for_relay_refund_status(
-    relay: &hyperswitch_domain_models::relay::Relay,
-    force_sync: bool,
-) -> bool {
+fn should_call_connector_for_relay_refund_status(relay: &relay::Relay, force_sync: bool) -> bool {
     // This allows refund sync at connector level if force_sync is enabled, or
     // check if the refund is in terminal state
     !matches!(relay.status, RelayStatus::Failure | RelayStatus::Success) && force_sync
@@ -288,9 +400,9 @@ fn should_call_connector_for_relay_refund_status(
 pub async fn sync_relay_refund_with_gateway(
     state: &SessionState,
     merchant_account: &domain::MerchantAccount,
-    relay_record: &hyperswitch_domain_models::relay::Relay,
+    relay_record: &relay::Relay,
     connector_account: domain::MerchantConnectorAccount,
-) -> RouterResult<hyperswitch_domain_models::relay::RelayUpdate> {
+) -> RouterResult<relay::RelayUpdate> {
     let connector_id = &relay_record.connector_id;
     let merchant_id = merchant_account.get_id();
 
@@ -333,8 +445,7 @@ pub async fn sync_relay_refund_with_gateway(
     .await
     .to_refund_failed_response()?;
 
-    let relay_response =
-        hyperswitch_domain_models::relay::RelayUpdate::from(router_data_res.response);
+    let relay_response = relay::RelayUpdate::from(router_data_res.response);
 
     Ok(relay_response)
 }
diff --git a/crates/router/src/routes/relay.rs b/crates/router/src/routes/relay.rs
index cfc66253d50..dd079563a20 100644
--- a/crates/router/src/routes/relay.rs
+++ b/crates/router/src/routes/relay.rs
@@ -22,7 +22,7 @@ pub async fn relay(
         &req,
         payload,
         |state, auth: auth::AuthenticationData, req, _| {
-            relay::relay(
+            relay::relay_flow_decider(
                 state,
                 auth.merchant_account,
                 #[cfg(feature = "v1")]

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

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

## Description
<!-- Describe your changes in detail -->
This pr refactors the relay flow to add trait based implementation for it.

Changes made:-
1. `relay_flow_decider` decides the relay flow based on the relay types
2. Introduced `RelayInterface` trait that consists the methods required for different relay types
    a. `validate_relay_request` validates the relay requests for different type of relay types. It also provides default 
          implementation, if a new relay type does not have specific type.
    b. `get_domain_models` to convert the relay request form api models to domain models
    c. `process_relay` that consists the core functionality for every relay type
    d. `generate_response` relay type specific response
3. `Validate` trait provides the generic validation. This trait can be implemented for any type, that requires validation.

### 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`
3. `crates/router/src/configs`
4. `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)?
-->
-> Make a relay payment
```
curl --location 'http://localhost:8080/relay' \
--header 'Content-Type: application/json' \
--header 'x-profile-id: pro_Zv50p4TBhOIRJFnynXL7' \
--header 'api-key: <api-key>' \
--data '{
    "connector_id": "mca_5dVmrsd0IwgQAsu3uiUo",
    "connector_resource_id": "pi_3Qs0CqEOqOywnAIx0QJ1vo67",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD"
        }
    },
    "type": "refund"
}'
```
```
{
    "id": "relay_5nS1Mr5DHgWCkadiEVwY",
    "status": "success",
    "connector_resource_id": "pi_3Qs0CqEOqOywnAIx0QJ1vo67",
    "error": null,
    "connector_reference_id": "re_3Qs0CqEOqOywnAIx07ZIvgZ1",
    "connector_id": "mca_5dVmrsd0IwgQAsu3uiUo",
    "profile_id": "pro_Zv50p4TBhOIRJFnynXL7",
    "type": "refund",
    "data": {
        "refund": {
            "amount": 1,
            "currency": "USD",
            "reason": null
        }
    }
}
```


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