mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(refunds_v2): Add Refunds Retrieve and Refunds Sync Core flow (#7835)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,10 +1,17 @@
|
||||
use std::str::FromStr;
|
||||
use std::{fmt::Debug, str::FromStr};
|
||||
|
||||
use api_models::{enums::Connector, refunds::RefundErrorDetails};
|
||||
use common_utils::{id_type, types as common_utils_types};
|
||||
use error_stack::{report, ResultExt};
|
||||
use hyperswitch_domain_models::router_data::{ErrorResponse, RouterData};
|
||||
use hyperswitch_interfaces::integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject};
|
||||
use hyperswitch_domain_models::{
|
||||
router_data::{ErrorResponse, RouterData},
|
||||
router_data_v2::RefundFlowData,
|
||||
};
|
||||
use hyperswitch_interfaces::{
|
||||
api::{Connector as ConnectorTrait, ConnectorIntegration},
|
||||
connector_integration_v2::{ConnectorIntegrationV2, ConnectorV2},
|
||||
integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject},
|
||||
};
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use crate::{
|
||||
@ -202,20 +209,27 @@ pub async fn trigger_refund_to_gateway(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
async fn call_connector_service(
|
||||
async fn call_connector_service<F>(
|
||||
state: &SessionState,
|
||||
connector: &api::ConnectorData,
|
||||
add_access_token_result: types::AddAccessTokenResult,
|
||||
router_data: RouterData<api::Execute, types::RefundsData, types::RefundsResponseData>,
|
||||
router_data: RouterData<F, types::RefundsData, types::RefundsResponseData>,
|
||||
) -> Result<
|
||||
RouterData<api::Execute, types::RefundsData, types::RefundsResponseData>,
|
||||
RouterData<F, types::RefundsData, types::RefundsResponseData>,
|
||||
error_stack::Report<errors::ConnectorError>,
|
||||
> {
|
||||
>
|
||||
where
|
||||
F: Debug + Clone + 'static,
|
||||
dyn ConnectorTrait + Sync:
|
||||
ConnectorIntegration<F, types::RefundsData, types::RefundsResponseData>,
|
||||
dyn ConnectorV2 + Sync:
|
||||
ConnectorIntegrationV2<F, RefundFlowData, types::RefundsData, types::RefundsResponseData>,
|
||||
{
|
||||
if !(add_access_token_result.connector_supports_access_token
|
||||
&& router_data.access_token.is_none())
|
||||
{
|
||||
let connector_integration: services::BoxedRefundConnectorIntegrationInterface<
|
||||
api::Execute,
|
||||
F,
|
||||
types::RefundsData,
|
||||
types::RefundsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
@ -382,9 +396,12 @@ pub fn get_refund_update_for_refund_response_data(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn perform_integrity_check(
|
||||
mut router_data: RouterData<api::Execute, types::RefundsData, types::RefundsResponseData>,
|
||||
) -> RouterData<api::Execute, types::RefundsData, types::RefundsResponseData> {
|
||||
pub fn perform_integrity_check<F>(
|
||||
mut router_data: RouterData<F, types::RefundsData, types::RefundsResponseData>,
|
||||
) -> RouterData<F, types::RefundsData, types::RefundsResponseData>
|
||||
where
|
||||
F: Debug + Clone + 'static,
|
||||
{
|
||||
// Initiating Integrity check
|
||||
let integrity_result = check_refund_integrity(&router_data.request, &router_data.response);
|
||||
router_data.integrity_check = integrity_result;
|
||||
@ -447,6 +464,276 @@ where
|
||||
request.check_integrity(request, connector_refund_id.to_owned())
|
||||
}
|
||||
|
||||
// ********************************************** REFUND SYNC **********************************************
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn refund_retrieve_core_with_refund_id(
|
||||
state: SessionState,
|
||||
merchant_context: domain::MerchantContext,
|
||||
profile: domain::Profile,
|
||||
request: refunds::RefundsRetrieveRequest,
|
||||
) -> errors::RouterResponse<refunds::RefundResponse> {
|
||||
let refund_id = request.refund_id.clone();
|
||||
let db = &*state.store;
|
||||
let profile_id = profile.get_id().to_owned();
|
||||
let refund = db
|
||||
.find_refund_by_id(
|
||||
&refund_id,
|
||||
merchant_context.get_merchant_account().storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?;
|
||||
|
||||
let response = Box::pin(refund_retrieve_core(
|
||||
state.clone(),
|
||||
merchant_context,
|
||||
Some(profile_id),
|
||||
request,
|
||||
refund,
|
||||
))
|
||||
.await?;
|
||||
|
||||
api::RefundResponse::foreign_try_from(response).map(services::ApplicationResponse::Json)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn refund_retrieve_core(
|
||||
state: SessionState,
|
||||
merchant_context: domain::MerchantContext,
|
||||
profile_id: Option<id_type::ProfileId>,
|
||||
request: refunds::RefundsRetrieveRequest,
|
||||
refund: storage::Refund,
|
||||
) -> errors::RouterResult<storage::Refund> {
|
||||
let db = &*state.store;
|
||||
|
||||
let key_manager_state = &(&state).into();
|
||||
core_utils::validate_profile_id_from_auth_layer(profile_id, &refund)?;
|
||||
let payment_id = &refund.payment_id;
|
||||
let payment_intent = db
|
||||
.find_payment_intent_by_id(
|
||||
key_manager_state,
|
||||
payment_id,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
merchant_context.get_merchant_account().storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?;
|
||||
|
||||
let active_attempt_id = payment_intent
|
||||
.active_attempt_id
|
||||
.clone()
|
||||
.ok_or(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Active attempt id not found")?;
|
||||
|
||||
let payment_attempt = db
|
||||
.find_payment_attempt_by_id(
|
||||
key_manager_state,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
&active_attempt_id,
|
||||
merchant_context.get_merchant_account().storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)?;
|
||||
|
||||
let unified_translated_message = if let (Some(unified_code), Some(unified_message)) =
|
||||
(refund.unified_code.clone(), refund.unified_message.clone())
|
||||
{
|
||||
helpers::get_unified_translation(
|
||||
&state,
|
||||
unified_code,
|
||||
unified_message.clone(),
|
||||
state.locale.to_string(),
|
||||
)
|
||||
.await
|
||||
.or(Some(unified_message))
|
||||
} else {
|
||||
refund.unified_message
|
||||
};
|
||||
|
||||
let refund = storage::Refund {
|
||||
unified_message: unified_translated_message,
|
||||
..refund
|
||||
};
|
||||
|
||||
let response = if should_call_refund(&refund, request.force_sync.unwrap_or(false)) {
|
||||
Box::pin(sync_refund_with_gateway(
|
||||
&state,
|
||||
&merchant_context,
|
||||
&payment_attempt,
|
||||
&payment_intent,
|
||||
&refund,
|
||||
))
|
||||
.await
|
||||
} else {
|
||||
Ok(refund)
|
||||
}?;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn should_call_refund(refund: &diesel_models::refund::Refund, force_sync: bool) -> bool {
|
||||
// This implies, we cannot perform a refund sync & `the connector_refund_id`
|
||||
// doesn't exist
|
||||
let predicate1 = refund.connector_refund_id.is_some();
|
||||
|
||||
// This allows refund sync at connector level if force_sync is enabled, or
|
||||
// checks if the refund has failed
|
||||
let predicate2 = force_sync
|
||||
|| !matches!(
|
||||
refund.refund_status,
|
||||
diesel_models::enums::RefundStatus::Failure
|
||||
| diesel_models::enums::RefundStatus::Success
|
||||
);
|
||||
|
||||
predicate1 && predicate2
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip_all)]
|
||||
pub async fn sync_refund_with_gateway(
|
||||
state: &SessionState,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
payment_attempt: &storage::PaymentAttempt,
|
||||
payment_intent: &storage::PaymentIntent,
|
||||
refund: &storage::Refund,
|
||||
) -> errors::RouterResult<storage::Refund> {
|
||||
let db = &*state.store;
|
||||
|
||||
let connector_id = refund.connector.to_string();
|
||||
let connector: api::ConnectorData = api::ConnectorData::get_connector_by_name(
|
||||
&state.conf.connectors,
|
||||
&connector_id,
|
||||
api::GetToken::Connector,
|
||||
payment_attempt.merchant_connector_id.clone(),
|
||||
)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to get the connector")?;
|
||||
|
||||
let mca_id = payment_attempt.get_attempt_merchant_connector_account_id()?;
|
||||
|
||||
let mca = db
|
||||
.find_merchant_connector_account_by_id(
|
||||
&state.into(),
|
||||
&mca_id,
|
||||
merchant_context.get_merchant_key_store(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to fetch merchant connector account")?;
|
||||
|
||||
let connector_enum = mca.connector_name;
|
||||
|
||||
let mut router_data = core_utils::construct_refund_router_data::<api::RSync>(
|
||||
state,
|
||||
connector_enum,
|
||||
merchant_context,
|
||||
payment_intent,
|
||||
payment_attempt,
|
||||
refund,
|
||||
&mca,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let add_access_token_result =
|
||||
access_token::add_access_token(state, &connector, merchant_context, &router_data, None)
|
||||
.await?;
|
||||
|
||||
logger::debug!(refund_retrieve_router_data=?router_data);
|
||||
|
||||
access_token::update_router_data_with_access_token_result(
|
||||
&add_access_token_result,
|
||||
&mut router_data,
|
||||
&payments::CallConnectorAction::Trigger,
|
||||
);
|
||||
|
||||
let connector_response =
|
||||
call_connector_service(state, &connector, add_access_token_result, router_data)
|
||||
.await
|
||||
.to_refund_failed_response()?;
|
||||
|
||||
let connector_response = perform_integrity_check(connector_response);
|
||||
|
||||
let refund_update =
|
||||
build_refund_update_for_rsync(&connector, merchant_context, connector_response);
|
||||
|
||||
let response = state
|
||||
.store
|
||||
.update_refund(
|
||||
refund.to_owned(),
|
||||
refund_update,
|
||||
merchant_context.get_merchant_account().storage_scheme,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::RefundNotFound)
|
||||
.attach_printable_lazy(|| {
|
||||
format!(
|
||||
"Unable to update refund with refund_id: {}",
|
||||
refund.id.get_string_repr()
|
||||
)
|
||||
})?;
|
||||
|
||||
// Implement outgoing webhook here
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn build_refund_update_for_rsync(
|
||||
connector: &api::ConnectorData,
|
||||
merchant_context: &domain::MerchantContext,
|
||||
router_data_response: RouterData<api::RSync, types::RefundsData, types::RefundsResponseData>,
|
||||
) -> storage::RefundUpdate {
|
||||
let merchant_account = merchant_context.get_merchant_account();
|
||||
let storage_scheme = &merchant_context.get_merchant_account().storage_scheme;
|
||||
|
||||
match router_data_response.response {
|
||||
Err(error_message) => {
|
||||
let refund_status = match error_message.status_code {
|
||||
// marking failure for 2xx because this is genuine refund failure
|
||||
200..=299 => Some(enums::RefundStatus::Failure),
|
||||
_ => None,
|
||||
};
|
||||
let refund_error_message = error_message.reason.or(Some(error_message.message));
|
||||
let refund_error_code = Some(error_message.code);
|
||||
|
||||
storage::RefundUpdate::build_error_update_for_refund_failure(
|
||||
refund_status,
|
||||
refund_error_message,
|
||||
refund_error_code,
|
||||
storage_scheme,
|
||||
)
|
||||
}
|
||||
Ok(response) => match router_data_response.integrity_check.clone() {
|
||||
Err(err) => {
|
||||
metrics::INTEGRITY_CHECK_FAILED.add(
|
||||
1,
|
||||
router_env::metric_attributes!(
|
||||
("connector", connector.connector_name.to_string()),
|
||||
("merchant_id", merchant_account.get_id().clone()),
|
||||
),
|
||||
);
|
||||
|
||||
let connector_refund_id = err
|
||||
.connector_transaction_id
|
||||
.map(common_utils_types::ConnectorTransactionId::from);
|
||||
|
||||
storage::RefundUpdate::build_error_update_for_integrity_check_failure(
|
||||
err.field_names,
|
||||
connector_refund_id,
|
||||
storage_scheme,
|
||||
)
|
||||
}
|
||||
Ok(()) => {
|
||||
let connector_refund_id =
|
||||
common_utils_types::ConnectorTransactionId::from(response.connector_refund_id);
|
||||
|
||||
storage::RefundUpdate::build_refund_update(
|
||||
connector_refund_id,
|
||||
response.refund_status,
|
||||
storage_scheme,
|
||||
)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ********************************************** VALIDATIONS **********************************************
|
||||
|
||||
#[instrument(skip_all)]
|
||||
|
||||
@ -252,7 +252,7 @@ pub async fn construct_refund_router_data<'a, F>(
|
||||
let status = payment_attempt.status;
|
||||
|
||||
let payment_amount = payment_attempt.get_total_amount();
|
||||
let currency = payment_intent.amount_details.currency;
|
||||
let currency = payment_intent.get_currency();
|
||||
|
||||
let payment_method_type = payment_attempt.payment_method_type;
|
||||
|
||||
|
||||
@ -1174,7 +1174,9 @@ impl Refunds {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
let mut route = web::scope("/v2/refunds").app_data(web::Data::new(state));
|
||||
|
||||
route = route.service(web::resource("").route(web::post().to(refunds::refunds_create)));
|
||||
route = route
|
||||
.service(web::resource("").route(web::post().to(refunds::refunds_create)))
|
||||
.service(web::resource("/{id}").route(web::get().to(refunds::refunds_retrieve)));
|
||||
|
||||
route
|
||||
}
|
||||
|
||||
@ -168,6 +168,56 @@ pub async fn refunds_retrieve(
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "v2", feature = "refunds_v2"))]
|
||||
#[instrument(skip_all, fields(flow))]
|
||||
pub async fn refunds_retrieve(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<common_utils::id_type::GlobalRefundId>,
|
||||
query_params: web::Query<api_models::refunds::RefundsRetrieveBody>,
|
||||
) -> HttpResponse {
|
||||
let refund_request = refunds::RefundsRetrieveRequest {
|
||||
refund_id: path.into_inner(),
|
||||
force_sync: query_params.force_sync,
|
||||
};
|
||||
let flow = match query_params.force_sync {
|
||||
Some(true) => Flow::RefundsRetrieveForceSync,
|
||||
_ => Flow::RefundsRetrieve,
|
||||
};
|
||||
|
||||
tracing::Span::current().record("flow", flow.to_string());
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
refund_request,
|
||||
|state, auth: auth::AuthenticationData, refund_request, _| {
|
||||
let merchant_context = domain::MerchantContext::NormalMerchant(Box::new(
|
||||
domain::Context(auth.merchant_account, auth.key_store),
|
||||
));
|
||||
refund_retrieve_core_with_refund_id(
|
||||
state,
|
||||
merchant_context,
|
||||
auth.profile,
|
||||
refund_request,
|
||||
)
|
||||
},
|
||||
auth::auth_type(
|
||||
&auth::V2ApiKeyAuth {
|
||||
is_connected_allowed: false,
|
||||
is_platform_allowed: false,
|
||||
},
|
||||
&auth::JWTAuth {
|
||||
permission: Permission::ProfileRefundRead,
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(all(any(feature = "v1", feature = "v2"), not(feature = "refunds_v2")))]
|
||||
/// Refunds - Retrieve (POST)
|
||||
///
|
||||
|
||||
@ -3,7 +3,8 @@ pub use api_models::refunds::RefundRequest;
|
||||
#[cfg(all(feature = "v2", feature = "refunds_v2"))]
|
||||
pub use api_models::refunds::RefundsCreateRequest;
|
||||
pub use api_models::refunds::{
|
||||
RefundResponse, RefundStatus, RefundType, RefundUpdateRequest, RefundsRetrieveRequest,
|
||||
RefundResponse, RefundStatus, RefundType, RefundUpdateRequest, RefundsRetrieveBody,
|
||||
RefundsRetrieveRequest,
|
||||
};
|
||||
pub use hyperswitch_domain_models::router_flow_types::refunds::{Execute, RSync};
|
||||
pub use hyperswitch_interfaces::api::refunds::{Refund, RefundExecute, RefundSync};
|
||||
|
||||
Reference in New Issue
Block a user