feat(connector): [XENDIT] Added Integrity Check for Authorize, Capture, Refund & RSync flows (#8049)

Co-authored-by: Sayak Bhattacharya <sayak.b@Sayak-Bhattacharya-G092THXJ34.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Sayak Bhattacharya
2025-05-23 14:51:00 +05:30
committed by GitHub
parent 9f9fef492b
commit caa0723502
4 changed files with 173 additions and 100 deletions

View File

@ -57,6 +57,7 @@ use transformers::{self as xendit, XenditEventType, XenditWebhookEvent};
use crate::{
constants::headers,
types::ResponseRouterData,
utils as connector_utils,
utils::{self, PaymentMethodDataType, RefundsRequestData},
};
@ -301,14 +302,27 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
.response
.parse_struct("XenditPaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_authorise_integrity_object(
self.amount_converter,
response.amount,
response.currency.to_string().clone(),
)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
let new_router_data = RouterData::try_from(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
})
}
fn get_error_response(
@ -478,20 +492,38 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Xen
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<PaymentsSyncRouterData, errors::ConnectorError> {
let response: xendit::XenditResponse =
res.response
.parse_struct("xendit XenditResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response: xendit::XenditResponse = res
.response
.clone()
.parse_struct("xendit XenditResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = match response.clone() {
xendit::XenditResponse::Payment(p) => connector_utils::get_sync_integrity_object(
self.amount_converter,
p.amount,
p.currency.to_string().clone(),
),
xendit::XenditResponse::Webhook(p) => connector_utils::get_sync_integrity_object(
self.amount_converter,
p.data.amount,
p.data.currency.to_string().clone(),
),
};
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
let new_router_data = RouterData::try_from(ResponseRouterData {
response,
data: data.clone(),
http_code: res.status_code,
});
new_router_data.and_then(|mut router_data| {
let integrity_object = response_integrity_object?;
router_data.request.integrity_object = Some(integrity_object);
Ok(router_data)
})
.change_context(errors::ConnectorError::ResponseHandlingFailed)
}
}
@ -599,15 +631,26 @@ impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> fo
.parse_struct("Xendit PaymentsResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_capture_integrity_object(
self.amount_converter,
Some(response.amount),
response.currency.to_string().clone(),
)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
let new_router_data = RouterData::try_from(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(
@ -695,15 +738,27 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Xendit
.parse_struct("xendit RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let response_integrity_object = connector_utils::get_refund_integrity_object(
self.amount_converter,
response.amount,
response.currency.to_string().clone(),
)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
let new_router_data = RouterData::try_from(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(
@ -772,19 +827,32 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Xendit {
event_builder: Option<&mut ConnectorEvent>,
res: Response,
) -> CustomResult<RefundSyncRouterData, errors::ConnectorError> {
let response: xendit::RefundResponse =
res.response
.parse_struct("xendit RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response: xendit::RefundResponse = res
.response
.clone()
.parse_struct("xendit RefundResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_refund_integrity_object(
self.amount_converter,
response.amount,
response.currency.to_string().clone(),
)?;
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
RouterData::try_from(ResponseRouterData {
let new_router_data = RouterData::try_from(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

@ -1,7 +1,7 @@
use std::collections::HashMap;
use cards::CardNumber;
use common_enums::enums;
use common_enums::{enums, Currency};
use common_utils::{pii, request::Method, types::FloatMajorUnit};
use hyperswitch_domain_models::{
payment_method_data::PaymentMethodData,
@ -65,7 +65,7 @@ pub struct CardPaymentRequest {
#[derive(Serialize, Deserialize, Debug)]
pub struct MandatePaymentRequest {
pub amount: FloatMajorUnit,
pub currency: common_enums::Currency,
pub currency: Currency,
pub capture_method: String,
pub payment_method_id: Secret<String>,
pub channel_properties: ChannelProperties,
@ -81,7 +81,7 @@ pub struct XenditPaymentsCaptureRequest {
#[derive(Serialize, Deserialize, Debug)]
pub struct XenditPaymentsRequest {
pub amount: FloatMajorUnit,
pub currency: common_enums::Currency,
pub currency: Currency,
pub capture_method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub payment_method: Option<PaymentMethod>,
@ -97,7 +97,7 @@ pub struct XenditSplitRoute {
pub flat_amount: Option<FloatMajorUnit>,
#[serde(skip_serializing_if = "Option::is_none")]
pub percent_amount: Option<i64>,
pub currency: enums::Currency,
pub currency: Currency,
pub destination_account_id: String,
pub reference_id: String,
}
@ -164,7 +164,7 @@ pub enum PaymentStatus {
AwaitingCapture,
Verified,
}
#[derive(Debug, Deserialize, Serialize)]
#[derive(Debug, Deserialize, Serialize, Clone)]
#[serde(untagged)]
pub enum XenditResponse {
Payment(XenditPaymentResponse),
@ -178,6 +178,8 @@ pub struct XenditPaymentResponse {
pub payment_method: PaymentMethodInfo,
pub failure_code: Option<String>,
pub reference_id: Secret<String>,
pub amount: FloatMajorUnit,
pub currency: Currency,
}
fn map_payment_response_to_attempt_status(
@ -502,7 +504,7 @@ impl<F>
common_utils::types::AmountConvertor::convert_back(
&required_conversion_type,
amount,
item.data.request.currency.unwrap_or(enums::Currency::USD),
item.data.request.currency.unwrap_or(Currency::USD),
)
.map_err(|_| {
errors::ConnectorError::RequestEncodingFailedWithReason(
@ -660,7 +662,7 @@ impl TryFrom<&PaymentsPreProcessingRouterData> for XenditSplitRequestData {
common_utils::types::AmountConvertor::convert(
&required_conversion_type,
amount,
item.request.currency.unwrap_or(enums::Currency::USD),
item.request.currency.unwrap_or(Currency::USD),
)
.map_err(|_| {
errors::ConnectorError::RequestEncodingFailedWithReason(
@ -739,8 +741,10 @@ impl From<RefundStatus> for enums::RefundStatus {
//TODO: Fill the struct with respective fields
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefundResponse {
id: String,
status: RefundStatus,
pub id: String,
pub status: RefundStatus,
pub amount: FloatMajorUnit,
pub currency: String,
}
impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>> for RefundsRouterData<Execute> {
@ -777,20 +781,22 @@ impl TryFrom<RefundsResponseRouterData<RSync, RefundResponse>> for RefundsRouter
pub struct XenditMetadata {
pub for_user_id: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct XenditWebhookEvent {
pub event: XenditEventType,
pub data: EventDetails,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct EventDetails {
pub id: String,
pub payment_request_id: Option<String>,
pub amount: FloatMajorUnit,
pub currency: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum XenditEventType {
#[serde(rename = "payment.succeeded")]
PaymentSucceeded,

View File

@ -6309,6 +6309,74 @@ pub fn get_card_details(
}
}
pub fn get_authorise_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: String,
) -> Result<AuthoriseIntegrityObject, 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)?;
Ok(AuthoriseIntegrityObject {
amount: amount_in_minor_unit,
currency: currency_enum,
})
}
pub fn get_sync_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: String,
) -> 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)?;
Ok(SyncIntegrityObject {
amount: Some(amount_in_minor_unit),
currency: Some(currency_enum),
})
}
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,
})
}
#[cfg(feature = "frm")]
pub trait FraudCheckSaleRequest {
fn get_order_details(&self) -> Result<Vec<OrderDetailsWithAmount>, Error>;
@ -6366,71 +6434,3 @@ impl SplitPaymentData for SetupMandateRequestData {
None
}
}
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,
})
}
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_sync_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: String,
) -> 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)?;
Ok(SyncIntegrityObject {
amount: Some(amount_in_minor_unit),
currency: Some(currency_enum),
})
}
pub fn get_authorise_integrity_object<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: String,
) -> Result<AuthoriseIntegrityObject, 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)?;
Ok(AuthoriseIntegrityObject {
amount: amount_in_minor_unit,
currency: currency_enum,
})
}

View File

@ -2750,7 +2750,6 @@ pub fn convert_back_amount_to_minor_units<T>(
.convert_back(amount, currency)
.change_context(errors::ConnectorError::AmountConversionFailed)
}
pub trait NetworkTokenData {
fn get_card_issuer(&self) -> Result<CardIssuer, Error>;
fn get_expiry_year_4_digit(&self) -> Secret<String>;