refactor(relay): add trait based implementation for relay (#7264)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Shankar Singh C
2025-03-03 12:22:27 +05:30
committed by GitHub
parent 96a11ac1c9
commit cdfbb82ffa
7 changed files with 261 additions and 118 deletions

View File

@ -23952,7 +23952,7 @@
],
"properties": {
"refund": {
"$ref": "#/components/schemas/RelayRefundRequest"
"$ref": "#/components/schemas/RelayRefundRequestData"
}
}
}
@ -23975,7 +23975,7 @@
}
}
},
"RelayRefundRequest": {
"RelayRefundRequestData": {
"type": "object",
"required": [
"amount",

View File

@ -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,

View File

@ -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")]

View File

@ -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,

View File

@ -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,

View File

@ -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)
}

View File

@ -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")]