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:
Sahkal Poddar
2024-07-08 20:39:58 +05:30
committed by GitHub
parent 2d31d38c1e
commit adc760f0a6
17 changed files with 427 additions and 44 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),
},
},
};

View File

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