From cdfbb82ffa893d65d1707d6795f07c0a71e8d0a9 Mon Sep 17 00:00:00 2001 From: Shankar Singh C <83439957+ShankarSinghC@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:22:27 +0530 Subject: [PATCH] refactor(relay): add trait based implementation for relay (#7264) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- api-reference/openapi_spec.json | 4 +- crates/api_models/src/relay.rs | 4 +- .../src/merchant_connector_account.rs | 8 + crates/hyperswitch_domain_models/src/relay.rs | 26 +- crates/openapi/src/openapi.rs | 2 +- crates/router/src/core/relay.rs | 333 ++++++++++++------ crates/router/src/routes/relay.rs | 2 +- 7 files changed, 261 insertions(+), 118 deletions(-) diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 0473fec243..060eb7038a 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -23952,7 +23952,7 @@ ], "properties": { "refund": { - "$ref": "#/components/schemas/RelayRefundRequest" + "$ref": "#/components/schemas/RelayRefundRequestData" } } } @@ -23975,7 +23975,7 @@ } } }, - "RelayRefundRequest": { + "RelayRefundRequestData": { "type": "object", "required": [ "amount", diff --git a/crates/api_models/src/relay.rs b/crates/api_models/src/relay.rs index f54e147163..aded73b434 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 9208bb486f..1e602c0282 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 { 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 { 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 8af58265c3..d4829c7599 100644 --- a/crates/hyperswitch_domain_models/src/relay.rs +++ b/crates/hyperswitch_domain_models/src/relay.rs @@ -74,6 +74,16 @@ impl From for RelayData { } } +impl From 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, @@ -92,6 +102,20 @@ impl RelayUpdate { } } +impl From 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 for api_models::relay::RelayResponse { fn from(value: Relay) -> Self { let error = value @@ -106,7 +130,7 @@ impl From 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 8d18d1fe59..a98080aa6f 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -546,7 +546,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 9492816452..474c371b7c 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, + 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; + + fn generate_response(value: relay::Relay) -> RouterResult; +} + +pub struct RelayRequestInner { + pub connector_resource_id: String, + pub connector_id: id_type::MerchantConnectorAccountId, + pub relay_type: PhantomData, + pub data: T::Request, +} + +impl RelayRequestInner { + pub fn from_relay_request(relay_request: relay_api_models::RelayRequest) -> RouterResult { + 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, + 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 { + 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 { + 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, key_store: domain::MerchantKeyStore, - req: relay_models::RelayRequest, -) -> RouterResponse { + request: relay_api_models::RelayRequest, +) -> RouterResponse { + let relay_flow_request = match request.relay_type { + common_enums::RelayType::Refund => { + RelayRequestInner::::from_relay_request(request)? + } + }; + relay( + state, + merchant_account, + profile_id_optional, + key_store, + relay_flow_request, + ) + .await +} + +pub async fn relay( + state: SessionState, + merchant_account: domain::MerchantAccount, + profile_id_optional: Option, + key_store: domain::MerchantKeyStore, + req: RelayRequestInner, +) -> RouterResponse { 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 { - 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, key_store: domain::MerchantKeyStore, - req: relay_models::RelayRetrieveRequest, -) -> RouterResponse { + req: relay_api_models::RelayRetrieveRequest, +) -> RouterResponse { 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 { +) -> RouterResult { 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 cfc66253d5..dd079563a2 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")]