feat(connector): [FISERV] Added Integrity Check support for all Payment & Refund Flows (#8075)

Co-authored-by: Sayak Bhattacharya <sayak.b@Sayak-Bhattacharya-G092THXJ34.local>
This commit is contained in:
Sayak Bhattacharya
2025-06-02 12:06:08 +05:30
committed by GitHub
parent b5011296ab
commit dbca363f44
2 changed files with 222 additions and 48 deletions

View File

@ -51,6 +51,7 @@ use uuid::Uuid;
use crate::{
constants::headers,
types::ResponseRouterData,
utils as connector_utils,
utils::{construct_not_implemented_error_report, convert_amount},
};
@ -168,18 +169,34 @@ impl ConnectorCommon for Fiserv {
event_builder.map(|i| i.set_error_response_body(&response));
router_env::logger::info!(connector_response=?response);
let fiserv::ErrorResponse { error, details } = response;
let error_details_opt = response.error.as_ref().and_then(|v| v.first());
Ok(error
.or(details)
.and_then(|error_details| {
error_details.first().map(|first_error| ErrorResponse {
code: first_error
let (code, message, reason) = if let Some(first_error) = error_details_opt {
let code = first_error
.code
.to_owned()
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()),
message: first_error.message.to_owned(),
reason: first_error.field.to_owned(),
.clone()
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string());
let message = first_error
.message
.clone()
.unwrap_or_else(|| consts::NO_ERROR_CODE.to_string());
let reason = first_error.additional_info.clone();
(code, message, reason)
} else {
(
consts::NO_ERROR_CODE.to_string(),
consts::NO_ERROR_MESSAGE.to_string(),
None,
)
};
Ok(ErrorResponse {
code,
message,
reason,
status_code: res.status_code,
attempt_status: None,
connector_transaction_id: None,
@ -187,18 +204,6 @@ impl ConnectorCommon for Fiserv {
network_decline_code: None,
network_error_message: None,
})
})
.unwrap_or(ErrorResponse {
code: consts::NO_ERROR_CODE.to_string(),
message: consts::NO_ERROR_MESSAGE.to_string(),
reason: None,
status_code: res.status_code,
attempt_status: None,
connector_transaction_id: None,
network_advice_code: None,
network_decline_code: None,
network_error_message: None,
}))
}
}
@ -404,14 +409,38 @@ impl ConnectorIntegration<PSync, PaymentsSyncData, PaymentsResponseData> for Fis
.response
.parse_struct("Fiserv PaymentSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let p_sync_response = response.sync_responses.first().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "P_Sync_Responses[0]",
},
)?;
let response_integrity_object = connector_utils::get_sync_integrity_object(
self.amount_converter,
p_sync_response.payment_receipt.approved_amount.total,
p_sync_response
.payment_receipt
.approved_amount
.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(
@ -483,16 +512,30 @@ impl ConnectorIntegration<Capture, PaymentsCaptureData, PaymentsResponseData> fo
.response
.parse_struct("Fiserv Payment Response")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_capture_integrity_object(
self.amount_converter,
Some(response.payment_receipt.approved_amount.total),
response
.payment_receipt
.approved_amount
.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_url(
@ -595,14 +638,32 @@ impl ConnectorIntegration<Authorize, PaymentsAuthorizeData, PaymentsResponseData
.response
.parse_struct("Fiserv PaymentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_authorise_integrity_object(
self.amount_converter,
response.payment_receipt.approved_amount.total,
response
.payment_receipt
.approved_amount
.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(
@ -684,14 +745,31 @@ impl ConnectorIntegration<Execute, RefundsData, RefundsResponseData> for Fiserv
res.response
.parse_struct("fiserv RefundResponse")
.change_context(errors::ConnectorError::RequestEncodingFailed)?;
let response_integrity_object = connector_utils::get_refund_integrity_object(
self.amount_converter,
response.payment_receipt.approved_amount.total,
response
.payment_receipt
.approved_amount
.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(
&self,
@ -767,14 +845,37 @@ impl ConnectorIntegration<RSync, RefundsData, RefundsResponseData> for Fiserv {
.response
.parse_struct("Fiserv Refund Response")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let r_sync_response = response.sync_responses.first().ok_or(
errors::ConnectorError::MissingRequiredField {
field_name: "R_Sync_Responses[0]",
},
)?;
let response_integrity_object = connector_utils::get_refund_integrity_object(
self.amount_converter,
r_sync_response.payment_receipt.approved_amount.total,
r_sync_response
.payment_receipt
.approved_amount
.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(

View File

@ -1,4 +1,4 @@
use common_enums::enums;
use common_enums::{enums, Currency};
use common_utils::{ext_traits::ValueExt, pii, types::FloatMajorUnit};
use error_stack::ResultExt;
use hyperswitch_domain_models::{
@ -274,7 +274,6 @@ impl TryFrom<&types::PaymentsCancelRouterData> for FiservCancelRequest {
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorResponse {
pub details: Option<Vec<ErrorDetails>>,
pub error: Option<Vec<ErrorDetails>>,
}
@ -282,10 +281,11 @@ pub struct ErrorResponse {
#[serde(rename_all = "camelCase")]
pub struct ErrorDetails {
#[serde(rename = "type")]
pub error_type: String,
pub error_type: Option<String>,
pub code: Option<String>,
pub message: String,
pub field: Option<String>,
pub message: Option<String>,
pub additional_info: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
@ -325,17 +325,89 @@ impl From<FiservPaymentStatus> for enums::RefundStatus {
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ProcessorResponseDetails {
pub approval_status: Option<String>,
pub approval_code: Option<String>,
pub reference_number: Option<String>,
pub processor: Option<String>,
pub host: Option<String>,
pub network_routed: Option<String>,
pub network_international_id: Option<String>,
pub response_code: Option<String>,
pub response_message: Option<String>,
pub host_response_code: Option<String>,
pub host_response_message: Option<String>,
pub additional_info: Option<Vec<AdditionalInfo>>,
pub bank_association_details: Option<BankAssociationDetails>,
pub response_indicators: Option<ResponseIndicators>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AdditionalInfo {
pub name: Option<String>,
pub value: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct BankAssociationDetails {
pub association_response_code: Option<String>,
pub avs_security_code_response: Option<AvsSecurityCodeResponse>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct AvsSecurityCodeResponse {
pub street_match: Option<String>,
pub postal_code_match: Option<String>,
pub security_code_match: Option<String>,
pub association: Option<Association>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct Association {
pub avs_code: Option<String>,
pub security_code_response: Option<String>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ResponseIndicators {
pub alternate_route_debit_indicator: Option<bool>,
pub signature_line_indicator: Option<bool>,
pub signature_debit_route_indicator: Option<bool>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct FiservPaymentsResponse {
gateway_response: GatewayResponse,
pub gateway_response: GatewayResponse,
pub payment_receipt: PaymentReceipt,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PaymentReceipt {
pub approved_amount: ApprovedAmount,
pub processor_response_details: Option<ProcessorResponseDetails>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct ApprovedAmount {
pub total: FloatMajorUnit,
pub currency: Currency,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
#[serde(transparent)]
pub struct FiservSyncResponse {
sync_responses: Vec<FiservPaymentsResponse>,
pub sync_responses: Vec<FiservPaymentsResponse>,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
@ -591,7 +663,8 @@ impl<F> TryFrom<&FiservRouterData<&types::RefundsRouterData<F>>> for FiservRefun
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct RefundResponse {
gateway_response: GatewayResponse,
pub gateway_response: GatewayResponse,
pub payment_receipt: PaymentReceipt,
}
impl TryFrom<RefundsResponseRouterData<Execute, RefundResponse>>