mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 11:06:50 +08:00
feat(router): add integrity check for refund refund sync and capture flow with stripe as connector (#5187)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Co-authored-by: Hrithikesh <61539176+hrithikesh026@users.noreply.github.com> Co-authored-by: Narayan Bhat <narayan.bhat@juspay.in> Co-authored-by: Sahkal Poddar <sahkalpoddar@Sahkals-MacBook-Air.local>
This commit is contained in:
@ -681,15 +681,26 @@ impl
|
||||
.parse_struct("PaymentIntentResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
let response_integrity_object = connector_utils::get_capture_integrity_object(
|
||||
self.amount_converter,
|
||||
response.amount_received,
|
||||
response.currency.clone(),
|
||||
)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
let new_router_data = types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed);
|
||||
|
||||
new_router_data.map(|mut router_data| {
|
||||
router_data.request.integrity_object = Some(response_integrity_object);
|
||||
router_data
|
||||
})
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -831,6 +842,7 @@ impl
|
||||
self.amount_converter,
|
||||
response.amount,
|
||||
response.currency.clone(),
|
||||
response.amount_received,
|
||||
)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
@ -1479,8 +1491,16 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
|
||||
req: &types::RefundsRouterData<api::Execute>,
|
||||
_connectors: &settings::Connectors,
|
||||
) -> CustomResult<RequestContent, errors::ConnectorError> {
|
||||
let refund_amount = connector_utils::convert_amount(
|
||||
self.amount_converter,
|
||||
req.request.minor_refund_amount,
|
||||
req.request.currency,
|
||||
)?;
|
||||
let request_body = match req.request.charges.as_ref() {
|
||||
None => RequestContent::FormUrlEncoded(Box::new(stripe::RefundRequest::try_from(req)?)),
|
||||
None => RequestContent::FormUrlEncoded(Box::new(stripe::RefundRequest::try_from((
|
||||
req,
|
||||
refund_amount,
|
||||
))?)),
|
||||
Some(_) => RequestContent::FormUrlEncoded(Box::new(
|
||||
stripe::ChargeRefundRequest::try_from(req)?,
|
||||
)),
|
||||
@ -1519,15 +1539,27 @@ impl services::ConnectorIntegration<api::Execute, types::RefundsData, types::Ref
|
||||
.parse_struct("Stripe RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
let response_integrity_object = connector_utils::get_refund_integrity_object(
|
||||
self.amount_converter,
|
||||
response.amount,
|
||||
response.currency.clone(),
|
||||
)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
let new_router_data = types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
});
|
||||
|
||||
new_router_data
|
||||
.map(|mut router_data| {
|
||||
router_data.request.integrity_object = Some(response_integrity_object);
|
||||
router_data
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
@ -1631,15 +1663,27 @@ impl services::ConnectorIntegration<api::RSync, types::RefundsData, types::Refun
|
||||
.parse_struct("Stripe RefundResponse")
|
||||
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
|
||||
|
||||
let response_integrity_object = connector_utils::get_refund_integrity_object(
|
||||
self.amount_converter,
|
||||
response.amount,
|
||||
response.currency.clone(),
|
||||
)?;
|
||||
|
||||
event_builder.map(|i| i.set_response_body(&response));
|
||||
router_env::logger::info!(connector_response=?response);
|
||||
|
||||
types::RouterData::try_from(types::ResponseRouterData {
|
||||
let new_router_data = types::RouterData::try_from(types::ResponseRouterData {
|
||||
response,
|
||||
data: data.clone(),
|
||||
http_code: res.status_code,
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
});
|
||||
|
||||
new_router_data
|
||||
.map(|mut router_data| {
|
||||
router_data.request.integrity_object = Some(response_integrity_object);
|
||||
router_data
|
||||
})
|
||||
.change_context(errors::ConnectorError::ResponseHandlingFailed)
|
||||
}
|
||||
|
||||
fn get_error_response(
|
||||
|
||||
@ -2876,13 +2876,14 @@ pub struct RefundRequest {
|
||||
pub meta_data: StripeMetadata,
|
||||
}
|
||||
|
||||
impl<F> TryFrom<&types::RefundsRouterData<F>> for RefundRequest {
|
||||
impl<F> TryFrom<(&types::RefundsRouterData<F>, MinorUnit)> for RefundRequest {
|
||||
type Error = error_stack::Report<errors::ConnectorError>;
|
||||
fn try_from(item: &types::RefundsRouterData<F>) -> Result<Self, Self::Error> {
|
||||
let amount = item.request.minor_refund_amount;
|
||||
fn try_from(
|
||||
(item, refund_amount): (&types::RefundsRouterData<F>, MinorUnit),
|
||||
) -> Result<Self, Self::Error> {
|
||||
let payment_intent = item.request.connector_transaction_id.clone();
|
||||
Ok(Self {
|
||||
amount: Some(amount),
|
||||
amount: Some(refund_amount),
|
||||
payment_intent,
|
||||
meta_data: StripeMetadata {
|
||||
order_id: Some(item.request.refund_id.clone()),
|
||||
|
||||
@ -23,7 +23,10 @@ use error_stack::{report, ResultExt};
|
||||
use hyperswitch_domain_models::{
|
||||
mandates,
|
||||
payments::payment_attempt::PaymentAttempt,
|
||||
router_request_types::{AuthoriseIntegrityObject, SyncIntegrityObject},
|
||||
router_request_types::{
|
||||
AuthoriseIntegrityObject, CaptureIntegrityObject, RefundIntegrityObject,
|
||||
SyncIntegrityObject,
|
||||
},
|
||||
};
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use once_cell::sync::Lazy;
|
||||
@ -2911,14 +2914,55 @@ pub fn get_sync_integrity_object<T>(
|
||||
amount_convertor: &dyn AmountConvertor<Output = T>,
|
||||
amount: T,
|
||||
currency: String,
|
||||
captured_amount: Option<T>,
|
||||
) -> Result<SyncIntegrityObject, error_stack::Report<errors::ConnectorError>> {
|
||||
let currency_enum = enums::Currency::from_str(currency.to_uppercase().as_str())
|
||||
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||
let amount_in_minor_unit =
|
||||
convert_back_amount_to_minor_units(amount_convertor, amount, currency_enum)?;
|
||||
|
||||
let capture_amount_in_minor_unit = captured_amount
|
||||
.map(|amount| convert_back_amount_to_minor_units(amount_convertor, amount, currency_enum))
|
||||
.transpose()?;
|
||||
|
||||
Ok(SyncIntegrityObject {
|
||||
amount: Some(amount_in_minor_unit),
|
||||
currency: Some(currency_enum),
|
||||
captured_amount: capture_amount_in_minor_unit,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_capture_integrity_object<T>(
|
||||
amount_convertor: &dyn AmountConvertor<Output = T>,
|
||||
capture_amount: Option<T>,
|
||||
currency: String,
|
||||
) -> Result<CaptureIntegrityObject, error_stack::Report<errors::ConnectorError>> {
|
||||
let currency_enum = enums::Currency::from_str(currency.to_uppercase().as_str())
|
||||
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||
|
||||
let capture_amount_in_minor_unit = capture_amount
|
||||
.map(|amount| convert_back_amount_to_minor_units(amount_convertor, amount, currency_enum))
|
||||
.transpose()?;
|
||||
|
||||
Ok(CaptureIntegrityObject {
|
||||
capture_amount: capture_amount_in_minor_unit,
|
||||
currency: currency_enum,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_refund_integrity_object<T>(
|
||||
amount_convertor: &dyn AmountConvertor<Output = T>,
|
||||
refund_amount: T,
|
||||
currency: String,
|
||||
) -> Result<RefundIntegrityObject, error_stack::Report<errors::ConnectorError>> {
|
||||
let currency_enum = enums::Currency::from_str(currency.to_uppercase().as_str())
|
||||
.change_context(errors::ConnectorError::ParsingFailed)?;
|
||||
|
||||
let refund_amount_in_minor_unit =
|
||||
convert_back_amount_to_minor_units(amount_convertor, refund_amount, currency_enum)?;
|
||||
|
||||
Ok(RefundIntegrityObject {
|
||||
currency: currency_enum,
|
||||
refund_amount: refund_amount_in_minor_unit,
|
||||
})
|
||||
}
|
||||
|
||||
@ -60,7 +60,7 @@ impl Feature<api::Capture, types::PaymentsCaptureData>
|
||||
types::PaymentsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
|
||||
let resp = services::execute_connector_processing_step(
|
||||
let mut new_router_data = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
&self,
|
||||
@ -70,7 +70,14 @@ impl Feature<api::Capture, types::PaymentsCaptureData>
|
||||
.await
|
||||
.to_payment_failed_response()?;
|
||||
|
||||
Ok(resp)
|
||||
// Initiating Integrity check
|
||||
let integrity_result = helpers::check_integrity_based_on_flow(
|
||||
&new_router_data.request,
|
||||
&new_router_data.response,
|
||||
);
|
||||
new_router_data.integrity_check = integrity_result;
|
||||
|
||||
Ok(new_router_data)
|
||||
}
|
||||
|
||||
async fn add_access_token<'a>(
|
||||
|
||||
@ -1372,6 +1372,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
|
||||
.as_ref()
|
||||
.map(|surcharge_details| surcharge_details.final_amount)
|
||||
.unwrap_or(payment_data.amount.into());
|
||||
let captured_amount = payment_data.payment_intent.amount_captured;
|
||||
Ok(Self {
|
||||
amount,
|
||||
integrity_object: None,
|
||||
@ -1394,6 +1395,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
|
||||
payment_method_type: payment_data.payment_attempt.payment_method_type,
|
||||
currency: payment_data.currency,
|
||||
payment_experience: payment_data.payment_attempt.payment_experience,
|
||||
captured_amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1515,6 +1517,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsCaptureD
|
||||
},
|
||||
browser_info,
|
||||
metadata: payment_data.payment_intent.metadata,
|
||||
integrity_object: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,8 @@ use common_utils::{
|
||||
};
|
||||
use diesel_models::process_tracker::business_status;
|
||||
use error_stack::{report, ResultExt};
|
||||
use hyperswitch_domain_models::router_data::ErrorResponse;
|
||||
use hyperswitch_interfaces::integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject};
|
||||
use masking::PeekInterface;
|
||||
use router_env::{instrument, metrics::add_attributes, tracing};
|
||||
use scheduler::{consumer::types::process_data, utils as process_tracker_utils};
|
||||
@ -234,6 +236,7 @@ pub async fn trigger_refund_to_gateway(
|
||||
),
|
||||
refund_error_code: Some("NOT_IMPLEMENTED".to_string()),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: None,
|
||||
})
|
||||
}
|
||||
errors::ConnectorError::NotSupported { message, connector } => {
|
||||
@ -244,6 +247,7 @@ pub async fn trigger_refund_to_gateway(
|
||||
)),
|
||||
refund_error_code: Some("NOT_SUPPORTED".to_string()),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
@ -266,7 +270,14 @@ pub async fn trigger_refund_to_gateway(
|
||||
)
|
||||
})?;
|
||||
}
|
||||
router_data_res.to_refund_failed_response()?
|
||||
let mut refund_router_data_res = router_data_res.to_refund_failed_response()?;
|
||||
// Initiating Integrity check
|
||||
let integrity_result = check_refund_integrity(
|
||||
&refund_router_data_res.request,
|
||||
&refund_router_data_res.response,
|
||||
);
|
||||
refund_router_data_res.integrity_check = integrity_result;
|
||||
refund_router_data_res
|
||||
} else {
|
||||
router_data
|
||||
};
|
||||
@ -277,22 +288,49 @@ pub async fn trigger_refund_to_gateway(
|
||||
refund_error_message: err.reason.or(Some(err.message)),
|
||||
refund_error_code: Some(err.code),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: None,
|
||||
},
|
||||
Ok(response) => {
|
||||
if response.refund_status == diesel_models::enums::RefundStatus::Success {
|
||||
metrics::SUCCESSFUL_REFUND.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([("connector", connector.connector_name.to_string())]),
|
||||
)
|
||||
}
|
||||
storage::RefundUpdate::Update {
|
||||
connector_refund_id: response.connector_refund_id,
|
||||
refund_status: response.refund_status,
|
||||
sent_to_gateway: true,
|
||||
refund_error_message: None,
|
||||
refund_arn: "".to_string(),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
// match on connector integrity checks
|
||||
match router_data_res.integrity_check.clone() {
|
||||
Err(err) => {
|
||||
let refund_connector_transaction_id = err.connector_transaction_id;
|
||||
metrics::INTEGRITY_CHECK_FAILED.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([
|
||||
("connector", connector.connector_name.to_string()),
|
||||
("merchant_id", merchant_account.merchant_id.clone()),
|
||||
]),
|
||||
);
|
||||
storage::RefundUpdate::ErrorUpdate {
|
||||
refund_status: Some(enums::RefundStatus::ManualReview),
|
||||
refund_error_message: Some(format!(
|
||||
"Integrity Check Failed! as data mismatched for fields {}",
|
||||
err.field_names
|
||||
)),
|
||||
refund_error_code: Some("IE".to_string()),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: refund_connector_transaction_id,
|
||||
}
|
||||
}
|
||||
Ok(()) => {
|
||||
if response.refund_status == diesel_models::enums::RefundStatus::Success {
|
||||
metrics::SUCCESSFUL_REFUND.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([("connector", connector.connector_name.to_string())]),
|
||||
)
|
||||
}
|
||||
storage::RefundUpdate::Update {
|
||||
connector_refund_id: response.connector_refund_id,
|
||||
refund_status: response.refund_status,
|
||||
sent_to_gateway: true,
|
||||
refund_error_message: None,
|
||||
refund_arn: "".to_string(),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -315,6 +353,22 @@ pub async fn trigger_refund_to_gateway(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
pub fn check_refund_integrity<T, Request>(
|
||||
request: &Request,
|
||||
refund_response_data: &Result<types::RefundsResponseData, ErrorResponse>,
|
||||
) -> Result<(), common_utils::errors::IntegrityCheckError>
|
||||
where
|
||||
T: FlowIntegrity,
|
||||
Request: GetIntegrityObject<T> + CheckIntegrity<Request, T>,
|
||||
{
|
||||
let connector_refund_id = refund_response_data
|
||||
.as_ref()
|
||||
.map(|resp_data| resp_data.connector_refund_id.clone())
|
||||
.ok();
|
||||
|
||||
request.check_integrity(request, connector_refund_id.to_owned())
|
||||
}
|
||||
|
||||
// ********************************************** REFUND SYNC **********************************************
|
||||
|
||||
pub async fn refund_response_wrapper<'a, F, Fut, T, Req>(
|
||||
@ -495,7 +549,7 @@ pub async fn sync_refund_with_gateway(
|
||||
types::RefundsData,
|
||||
types::RefundsResponseData,
|
||||
> = connector.connector.get_connector_integration();
|
||||
services::execute_connector_processing_step(
|
||||
let mut refund_sync_router_data = services::execute_connector_processing_step(
|
||||
state,
|
||||
connector_integration,
|
||||
&router_data,
|
||||
@ -503,7 +557,17 @@ pub async fn sync_refund_with_gateway(
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.to_refund_failed_response()?
|
||||
.to_refund_failed_response()?;
|
||||
|
||||
// Initiating connector integrity checks
|
||||
let integrity_result = check_refund_integrity(
|
||||
&refund_sync_router_data.request,
|
||||
&refund_sync_router_data.response,
|
||||
);
|
||||
|
||||
refund_sync_router_data.integrity_check = integrity_result;
|
||||
|
||||
refund_sync_router_data
|
||||
} else {
|
||||
router_data
|
||||
};
|
||||
@ -520,15 +584,39 @@ pub async fn sync_refund_with_gateway(
|
||||
refund_error_message: error_message.reason.or(Some(error_message.message)),
|
||||
refund_error_code: Some(error_message.code),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: None,
|
||||
}
|
||||
}
|
||||
Ok(response) => storage::RefundUpdate::Update {
|
||||
connector_refund_id: response.connector_refund_id,
|
||||
refund_status: response.refund_status,
|
||||
sent_to_gateway: true,
|
||||
refund_error_message: None,
|
||||
refund_arn: "".to_string(),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
Ok(response) => match router_data_res.integrity_check.clone() {
|
||||
Err(err) => {
|
||||
metrics::INTEGRITY_CHECK_FAILED.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&add_attributes([
|
||||
("connector", connector.connector_name.to_string()),
|
||||
("merchant_id", merchant_account.merchant_id.clone()),
|
||||
]),
|
||||
);
|
||||
let refund_connector_transaction_id = err.connector_transaction_id;
|
||||
storage::RefundUpdate::ErrorUpdate {
|
||||
refund_status: Some(enums::RefundStatus::ManualReview),
|
||||
refund_error_message: Some(format!(
|
||||
"Integrity Check Failed! as data mismatched for fields {}",
|
||||
err.field_names
|
||||
)),
|
||||
refund_error_code: Some("IE".to_string()),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
connector_refund_id: refund_connector_transaction_id,
|
||||
}
|
||||
}
|
||||
Ok(()) => storage::RefundUpdate::Update {
|
||||
connector_refund_id: response.connector_refund_id,
|
||||
refund_status: response.refund_status,
|
||||
sent_to_gateway: true,
|
||||
refund_error_message: None,
|
||||
refund_arn: "".to_string(),
|
||||
updated_by: storage_scheme.to_string(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -340,6 +340,7 @@ pub async fn construct_refund_router_data<'a, F>(
|
||||
connector_refund_id: refund.connector_refund_id.clone(),
|
||||
browser_info,
|
||||
charges,
|
||||
integrity_object: None,
|
||||
},
|
||||
|
||||
response: Ok(types::RefundsResponseData {
|
||||
|
||||
Reference in New Issue
Block a user