feat(core): Added integrity framework for Authorize and Sync flow with connector as Stripe (#5109)

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>
This commit is contained in:
Sahkal Poddar
2024-07-04 15:06:06 +05:30
committed by GitHub
parent 0796bb3b25
commit c8c0cb765e
37 changed files with 725 additions and 234 deletions

View File

@ -264,6 +264,12 @@ pub enum StripeErrorCode {
PaymentMethodDeleteFailed,
#[error(error_type = StripeErrorType::InvalidRequestError, code = "", message = "Extended card info does not exist")]
ExtendedCardInfoNotFound,
#[error(error_type = StripeErrorType::ConnectorError, code = "CE", message = "{reason} as data mismatched for {field_names}")]
IntegrityCheckFailed {
reason: String,
field_names: String,
connector_transaction_id: Option<String>,
},
#[error(error_type = StripeErrorType::InvalidRequestError, code = "IR_28", message = "Invalid tenant")]
InvalidTenant,
#[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")]
@ -650,6 +656,15 @@ impl From<errors::ApiErrorResponse> for StripeErrorCode {
Self::InvalidWalletToken { wallet_name }
}
errors::ApiErrorResponse::ExtendedCardInfoNotFound => Self::ExtendedCardInfoNotFound,
errors::ApiErrorResponse::IntegrityCheckFailed {
reason,
field_names,
connector_transaction_id,
} => Self::IntegrityCheckFailed {
reason,
field_names,
connector_transaction_id,
},
errors::ApiErrorResponse::InvalidTenant { tenant_id: _ }
| errors::ApiErrorResponse::MissingTenantId => Self::InvalidTenant,
errors::ApiErrorResponse::AmountConversionFailed { amount_type } => {
@ -741,6 +756,7 @@ impl actix_web::ResponseError for StripeErrorCode {
Self::ExternalConnectorError { status_code, .. } => {
StatusCode::from_u16(*status_code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
Self::IntegrityCheckFailed { .. } => StatusCode::INTERNAL_SERVER_ERROR,
Self::PaymentBlockedError { code, .. } => {
StatusCode::from_u16(*code).unwrap_or(StatusCode::OK)
}

View File

@ -298,7 +298,7 @@ impl ConnectorIntegration<api::Authorize, types::PaymentsAuthorizeData, types::P
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
let capture_amount_in_minor_units = match response.data.price_amount {
Some(ref amount) => Some(utils::convert_back(
Some(ref amount) => Some(utils::convert_back_amount_to_minor_units(
self.amount_converter,
amount.clone(),
data.request.currency,
@ -393,7 +393,7 @@ impl ConnectorIntegration<api::PSync, types::PaymentsSyncData, types::PaymentsRe
event_builder.map(|i| i.set_response_body(&response));
router_env::logger::info!(connector_response=?response);
let capture_amount_in_minor_units = match response.data.price_amount {
Some(ref amount) => Some(utils::convert_back(
Some(ref amount) => Some(utils::convert_back_amount_to_minor_units(
self.amount_converter,
amount.clone(),
data.request.currency,

View File

@ -827,13 +827,23 @@ impl
.parse_struct("PaymentIntentSyncResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_sync_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,
});
new_router_data.map(|mut router_data| {
router_data.request.integrity_object = Some(response_integrity_object);
router_data
})
}
Err(err) => {
@ -1011,13 +1021,25 @@ impl
.parse_struct("ChargesResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object =
connector_utils::get_authorise_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,
});
new_router_data.map(|mut router_data| {
router_data.request.integrity_object = Some(response_integrity_object);
router_data
})
}
_ => {
@ -1026,13 +1048,25 @@ impl
.parse_struct("PaymentIntentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object =
connector_utils::get_authorise_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,
});
new_router_data.map(|mut router_data| {
router_data.request.integrity_object = Some(response_integrity_object);
router_data
})
}
},
@ -1042,15 +1076,26 @@ impl
.parse_struct("PaymentIntentResponse")
.change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
let response_integrity_object = connector_utils::get_authorise_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)
.change_context(errors::ConnectorError::ResponseHandlingFailed);
new_router_data.map(|mut router_data| {
router_data.request.integrity_object = Some(response_integrity_object);
router_data
})
}
}
}

View File

@ -1,4 +1,7 @@
use std::collections::{HashMap, HashSet};
use std::{
collections::{HashMap, HashSet},
str::FromStr,
};
#[cfg(feature = "payouts")]
use api_models::payouts::{self, PayoutVendorAccountDetails};
@ -17,7 +20,11 @@ use common_utils::{
};
use diesel_models::enums;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::{mandates, payments::payment_attempt::PaymentAttempt};
use hyperswitch_domain_models::{
mandates,
payments::payment_attempt::PaymentAttempt,
router_request_types::{AuthoriseIntegrityObject, SyncIntegrityObject},
};
use masking::{ExposeInterface, Secret};
use once_cell::sync::Lazy;
use regex::Regex;
@ -2863,7 +2870,7 @@ pub fn convert_amount<T>(
.change_context(errors::ConnectorError::AmountConversionFailed)
}
pub fn convert_back<T>(
pub fn convert_back_amount_to_minor_units<T>(
amount_convertor: &dyn AmountConvertor<Output = T>,
amount: T,
currency: enums::Currency,
@ -2872,3 +2879,36 @@ pub fn convert_back<T>(
.convert_back(amount, currency)
.change_context(errors::ConnectorError::AmountConversionFailed)
}
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),
})
}

View File

@ -181,6 +181,7 @@ pub fn construct_router_data<F: Clone, Req, Res>(
refund_id: None,
payment_method_status: None,
connector_response: None,
integrity_check: Ok(()),
})
}

View File

@ -130,6 +130,7 @@ impl ConstructFlowSpecificData<frm_api::Checkout, FraudCheckCheckoutData, FraudC
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -111,6 +111,7 @@ pub async fn construct_fulfillment_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}

View File

@ -102,6 +102,7 @@ impl ConstructFlowSpecificData<RecordReturn, FraudCheckRecordReturnData, FraudCh
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -111,6 +111,7 @@ impl ConstructFlowSpecificData<frm_api::Sale, FraudCheckSaleData, FraudCheckResp
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -115,6 +115,7 @@ impl
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -75,6 +75,7 @@ pub async fn construct_mandate_revoke_router_data(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)

View File

@ -76,7 +76,7 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
if self.should_proceed_with_authorize() {
self.decide_authentication_type();
logger::debug!(auth_type=?self.auth_type);
let resp = services::execute_connector_processing_step(
let mut new_router_data = services::execute_connector_processing_step(
state,
connector_integration,
&self,
@ -86,8 +86,16 @@ impl Feature<api::Authorize, types::PaymentsAuthorizeData> for types::PaymentsAu
.await
.to_payment_failed_response()?;
// 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;
metrics::PAYMENT_COUNT.add(&metrics::CONTEXT, 1, &[]); // Metrics
Ok(resp)
Ok(new_router_data)
} else {
Ok(self.clone())
}

View File

@ -85,7 +85,7 @@ impl Feature<api::PSync, types::PaymentsSyncData>
(types::SyncRequestType::MultipleCaptureSync(_), Err(err)) => Err(err),
_ => {
// for bulk sync of captures, above logic needs to be handled at connector end
let resp = services::execute_connector_processing_step(
let mut new_router_data = services::execute_connector_processing_step(
state,
connector_integration,
&self,
@ -94,7 +94,16 @@ impl Feature<api::PSync, types::PaymentsSyncData>
)
.await
.to_payment_failed_response()?;
Ok(resp)
// Initiating Integrity checks
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)
}
}
}

View File

@ -21,6 +21,7 @@ use hyperswitch_domain_models::{
payments::{payment_attempt::PaymentAttempt, payment_intent::CustomerData, PaymentIntent},
router_data::KlarnaSdkResponse,
};
use hyperswitch_interfaces::integrity::{CheckIntegrity, FlowIntegrity, GetIntegrityObject};
use josekit::jwe;
use masking::{ExposeInterface, PeekInterface};
use openssl::{
@ -66,7 +67,7 @@ use crate::{
},
transformers::{ForeignFrom, ForeignTryFrom},
AdditionalPaymentMethodConnectorResponse, ErrorResponse, MandateReference,
RecurringMandatePaymentData, RouterData,
PaymentsResponseData, RecurringMandatePaymentData, RouterData,
},
utils::{
self,
@ -3523,6 +3524,7 @@ pub fn router_data_type_conversion<F1, F2, Req1, Req2, Res1, Res2>(
refund_id: router_data.refund_id,
dispute_id: router_data.dispute_id,
connector_response: router_data.connector_response,
integrity_check: Ok(()),
connector_wallets_details: router_data.connector_wallets_details,
}
}
@ -4946,6 +4948,35 @@ pub fn get_redis_key_for_extended_card_info(merchant_id: &str, payment_id: &str)
format!("{merchant_id}_{payment_id}_extended_card_info")
}
pub fn check_integrity_based_on_flow<T, Request>(
request: &Request,
payment_response_data: &Result<PaymentsResponseData, ErrorResponse>,
) -> Result<(), common_utils::errors::IntegrityCheckError>
where
T: FlowIntegrity,
Request: GetIntegrityObject<T> + CheckIntegrity<Request, T>,
{
let connector_transaction_id = match payment_response_data {
Ok(resp_data) => match resp_data {
PaymentsResponseData::TransactionResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
PaymentsResponseData::TransactionUnresolvedResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
PaymentsResponseData::PreProcessingResponse {
connector_response_reference_id,
..
} => connector_response_reference_id,
_ => &None,
},
Err(_) => &None,
};
request.check_integrity(request, connector_transaction_id.to_owned())
}
pub async fn config_skip_saving_wallet_at_connector(
db: &dyn StorageInterface,
merchant_id: &String,

View File

@ -7,7 +7,7 @@ use error_stack::{report, ResultExt};
use futures::FutureExt;
use hyperswitch_domain_models::payments::payment_attempt::PaymentAttempt;
use router_derive;
use router_env::{instrument, logger, tracing};
use router_env::{instrument, logger, metrics::add_attributes, tracing};
use storage_impl::DataModelExt;
use tracing_futures::Instrument;
@ -868,94 +868,11 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
};
(capture_update, attempt_update)
}
Ok(payments_response) => {
let attempt_status = payment_data.payment_attempt.status.to_owned();
let connector_status = router_data.status.to_owned();
let updated_attempt_status = match (
connector_status,
attempt_status,
payment_data.frm_message.to_owned(),
) {
(
enums::AttemptStatus::Authorized,
enums::AttemptStatus::Unresolved,
Some(frm_message),
) => match frm_message.frm_status {
enums::FraudCheckStatus::Fraud | enums::FraudCheckStatus::ManualReview => {
attempt_status
}
_ => router_data.get_attempt_status_for_db_update(&payment_data),
},
_ => router_data.get_attempt_status_for_db_update(&payment_data),
};
match payments_response {
types::PaymentsResponseData::PreProcessingResponse {
pre_processing_id,
connector_metadata,
connector_response_reference_id,
..
} => {
let connector_transaction_id = match pre_processing_id.to_owned() {
types::PreprocessingResponseId::PreProcessingId(_) => None,
types::PreprocessingResponseId::ConnectorTransactionId(
connector_txn_id,
) => Some(connector_txn_id),
};
let preprocessing_step_id = match pre_processing_id {
types::PreprocessingResponseId::PreProcessingId(pre_processing_id) => {
Some(pre_processing_id)
}
types::PreprocessingResponseId::ConnectorTransactionId(_) => None,
};
let payment_attempt_update =
storage::PaymentAttemptUpdate::PreprocessingUpdate {
status: updated_attempt_status,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
connector_metadata,
preprocessing_step_id,
connector_transaction_id,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
};
(None, Some(payment_attempt_update))
}
types::PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data,
connector_metadata,
connector_response_reference_id,
incremental_authorization_allowed,
charge_id,
..
} => {
payment_data
.payment_intent
.incremental_authorization_allowed =
core_utils::get_incremental_authorization_allowed_value(
incremental_authorization_allowed,
payment_data
.payment_intent
.request_incremental_authorization,
);
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let encoded_data = payment_data.payment_attempt.encoded_data.clone();
let authentication_data = redirection_data
.as_ref()
.map(Encode::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not parse the connector response")?;
// match on connector integrity check
match router_data.integrity_check.clone() {
Err(err) => {
let auth_update = if Some(router_data.auth_type)
!= payment_data.payment_attempt.authentication_type
{
@ -963,133 +880,259 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
} else {
None
};
// incase of success, update error code and error message
let error_status = if router_data.status == enums::AttemptStatus::Charged {
Some(None)
} else {
None
};
if router_data.status == enums::AttemptStatus::Charged {
payment_data
.payment_intent
.fingerprint_id
.clone_from(&payment_data.payment_attempt.fingerprint_id);
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
}
let payment_method_id = payment_data.payment_attempt.payment_method_id.clone();
utils::add_apple_pay_payment_status_metrics(
router_data.status,
router_data.apple_pay_flow.clone(),
payment_data.payment_attempt.connector.clone(),
payment_data.payment_attempt.merchant_id.clone(),
);
let (capture_updates, payment_attempt_update) = match payment_data
.multiple_capture_data
{
Some(multiple_capture_data) => {
let capture_update = storage::CaptureUpdate::ResponseUpdate {
status: enums::CaptureStatus::foreign_try_from(router_data.status)?,
connector_capture_id: connector_transaction_id.clone(),
connector_response_reference_id,
};
let capture_update_list = vec![(
multiple_capture_data.get_latest_capture().clone(),
capture_update,
)];
(
Some((multiple_capture_data, capture_update_list)),
auth_update.map(|auth_type| {
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type: auth_type,
updated_by: storage_scheme.to_string(),
}
}),
)
}
None => (
None,
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id: connector_transaction_id.clone(),
authentication_type: auth_update,
amount_capturable: router_data
.request
.get_amount_capturable(&payment_data, updated_attempt_status)
.map(MinorUnit::new),
payment_method_id,
mandate_id: payment_data.payment_attempt.mandate_id.clone(),
connector_metadata,
payment_token: None,
error_code: error_status.clone(),
error_message: error_status.clone(),
error_reason: error_status.clone(),
unified_code: error_status.clone(),
unified_message: error_status,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
authentication_data,
encoded_data,
payment_method_data: additional_payment_method_data,
charge_id,
}),
),
};
(capture_updates, payment_attempt_update)
}
types::PaymentsResponseData::TransactionUnresolvedResponse {
resource_id,
reason,
connector_response_reference_id,
} => {
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let field_name = err.field_names;
let connector_transaction_id = err.connector_transaction_id;
(
None,
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
status: updated_attempt_status,
Some(storage::PaymentAttemptUpdate::ErrorUpdate {
connector: None,
connector_transaction_id,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
error_code: Some(reason.clone().map(|cd| cd.code)),
error_message: Some(reason.clone().map(|cd| cd.message)),
error_reason: Some(reason.map(|cd| cd.message)),
connector_response_reference_id,
status: enums::AttemptStatus::Pending,
error_message: Some(Some("Integrity Check Failed!".to_string())),
error_code: Some(Some("IE".to_string())),
error_reason: Some(Some(format!(
"Integrity Check Failed! Value mismatched for fields {field_name}"
))),
amount_capturable: None,
updated_by: storage_scheme.to_string(),
unified_code: None,
unified_message: None,
connector_transaction_id,
payment_method_data: None,
authentication_type: auth_update,
}),
)
}
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
types::PaymentsResponseData::ConnectorCustomerResponse { .. } => (None, None),
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => (None, None),
types::PaymentsResponseData::IncrementalAuthorizationResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::MultipleCaptureResponse {
capture_sync_response_list,
} => match payment_data.multiple_capture_data {
Some(multiple_capture_data) => {
let capture_update_list = response_to_capture_update(
&multiple_capture_data,
Ok(()) => {
let attempt_status = payment_data.payment_attempt.status.to_owned();
let connector_status = router_data.status.to_owned();
let updated_attempt_status = match (
connector_status,
attempt_status,
payment_data.frm_message.to_owned(),
) {
(
enums::AttemptStatus::Authorized,
enums::AttemptStatus::Unresolved,
Some(frm_message),
) => match frm_message.frm_status {
enums::FraudCheckStatus::Fraud
| enums::FraudCheckStatus::ManualReview => attempt_status,
_ => router_data.get_attempt_status_for_db_update(&payment_data),
},
_ => router_data.get_attempt_status_for_db_update(&payment_data),
};
match payments_response {
types::PaymentsResponseData::PreProcessingResponse {
pre_processing_id,
connector_metadata,
connector_response_reference_id,
..
} => {
let connector_transaction_id = match pre_processing_id.to_owned() {
types::PreprocessingResponseId::PreProcessingId(_) => None,
types::PreprocessingResponseId::ConnectorTransactionId(
connector_txn_id,
) => Some(connector_txn_id),
};
let preprocessing_step_id = match pre_processing_id {
types::PreprocessingResponseId::PreProcessingId(
pre_processing_id,
) => Some(pre_processing_id),
types::PreprocessingResponseId::ConnectorTransactionId(_) => None,
};
let payment_attempt_update =
storage::PaymentAttemptUpdate::PreprocessingUpdate {
status: updated_attempt_status,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
connector_metadata,
preprocessing_step_id,
connector_transaction_id,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
};
(None, Some(payment_attempt_update))
}
types::PaymentsResponseData::TransactionResponse {
resource_id,
redirection_data,
connector_metadata,
connector_response_reference_id,
incremental_authorization_allowed,
charge_id,
..
} => {
payment_data
.payment_intent
.incremental_authorization_allowed =
core_utils::get_incremental_authorization_allowed_value(
incremental_authorization_allowed,
payment_data
.payment_intent
.request_incremental_authorization,
);
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
let encoded_data = payment_data.payment_attempt.encoded_data.clone();
let authentication_data = redirection_data
.as_ref()
.map(Encode::encode_to_value)
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Could not parse the connector response")?;
let auth_update = if Some(router_data.auth_type)
!= payment_data.payment_attempt.authentication_type
{
Some(router_data.auth_type)
} else {
None
};
// incase of success, update error code and error message
let error_status =
if router_data.status == enums::AttemptStatus::Charged {
Some(None)
} else {
None
};
if router_data.status == enums::AttemptStatus::Charged {
payment_data
.payment_intent
.fingerprint_id
.clone_from(&payment_data.payment_attempt.fingerprint_id);
metrics::SUCCESSFUL_PAYMENT.add(&metrics::CONTEXT, 1, &[]);
}
let payment_method_id =
payment_data.payment_attempt.payment_method_id.clone();
utils::add_apple_pay_payment_status_metrics(
router_data.status,
router_data.apple_pay_flow.clone(),
payment_data.payment_attempt.connector.clone(),
payment_data.payment_attempt.merchant_id.clone(),
);
let (capture_updates, payment_attempt_update) = match payment_data
.multiple_capture_data
{
Some(multiple_capture_data) => {
let capture_update = storage::CaptureUpdate::ResponseUpdate {
status: enums::CaptureStatus::foreign_try_from(
router_data.status,
)?,
connector_capture_id: connector_transaction_id.clone(),
connector_response_reference_id,
};
let capture_update_list = vec![(
multiple_capture_data.get_latest_capture().clone(),
capture_update,
)];
(Some((multiple_capture_data, capture_update_list)), auth_update.map(|auth_type| {
storage::PaymentAttemptUpdate::AuthenticationTypeUpdate {
authentication_type: auth_type,
updated_by: storage_scheme.to_string(),
}
}))
}
None => (
None,
Some(storage::PaymentAttemptUpdate::ResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id: connector_transaction_id.clone(),
authentication_type: auth_update,
amount_capturable: router_data
.request
.get_amount_capturable(
&payment_data,
updated_attempt_status,
)
.map(MinorUnit::new),
payment_method_id,
mandate_id: payment_data.payment_attempt.mandate_id.clone(),
connector_metadata,
payment_token: None,
error_code: error_status.clone(),
error_message: error_status.clone(),
error_reason: error_status.clone(),
unified_code: error_status.clone(),
unified_message: error_status,
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
authentication_data,
encoded_data,
payment_method_data: additional_payment_method_data,
charge_id,
}),
),
};
(capture_updates, payment_attempt_update)
}
types::PaymentsResponseData::TransactionUnresolvedResponse {
resource_id,
reason,
connector_response_reference_id,
} => {
let connector_transaction_id = match resource_id {
types::ResponseId::NoResponseId => None,
types::ResponseId::ConnectorTransactionId(id)
| types::ResponseId::EncodedData(id) => Some(id),
};
(
None,
Some(storage::PaymentAttemptUpdate::UnresolvedResponseUpdate {
status: updated_attempt_status,
connector: None,
connector_transaction_id,
payment_method_id: payment_data
.payment_attempt
.payment_method_id
.clone(),
error_code: Some(reason.clone().map(|cd| cd.code)),
error_message: Some(reason.clone().map(|cd| cd.message)),
error_reason: Some(reason.map(|cd| cd.message)),
connector_response_reference_id,
updated_by: storage_scheme.to_string(),
}),
)
}
types::PaymentsResponseData::SessionResponse { .. } => (None, None),
types::PaymentsResponseData::SessionTokenResponse { .. } => (None, None),
types::PaymentsResponseData::TokenizationResponse { .. } => (None, None),
types::PaymentsResponseData::ConnectorCustomerResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::ThreeDSEnrollmentResponse { .. } => {
(None, None)
}
types::PaymentsResponseData::IncrementalAuthorizationResponse {
..
} => (None, None),
types::PaymentsResponseData::MultipleCaptureResponse {
capture_sync_response_list,
)?;
(Some((multiple_capture_data, capture_update_list)), None)
} => match payment_data.multiple_capture_data {
Some(multiple_capture_data) => {
let capture_update_list = response_to_capture_update(
&multiple_capture_data,
capture_sync_response_list,
)?;
(Some((multiple_capture_data, capture_update_list)), None)
}
None => (None, None),
},
}
None => (None, None),
},
}
}
}
};
@ -1308,7 +1351,33 @@ async fn payment_response_update_tracker<F: Clone, T: types::Capturable>(
.as_mut()
.map(|info| info.status = status)
});
Ok(payment_data)
match router_data.integrity_check {
Ok(()) => Ok(payment_data),
Err(err) => {
metrics::INTEGRITY_CHECK_FAILED.add(
&metrics::CONTEXT,
1,
&add_attributes([
(
"connector",
payment_data.payment_attempt.connector.unwrap_or_default(),
),
("merchant_id", payment_data.payment_attempt.merchant_id),
]),
);
Err(error_stack::Report::new(
errors::ApiErrorResponse::IntegrityCheckFailed {
reason: payment_data
.payment_attempt
.error_message
.unwrap_or_default(),
field_names: err.field_names,
connector_transaction_id: payment_data.payment_attempt.connector_transaction_id,
},
))
}
}
}
async fn update_payment_method_status_and_ntid<F: Clone>(

View File

@ -195,6 +195,7 @@ where
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -1349,6 +1350,7 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsAuthoriz
.transpose()?,
customer_acceptance: payment_data.customer_acceptance,
charges,
integrity_object: None,
})
}
}
@ -1358,7 +1360,14 @@ impl<F: Clone> TryFrom<PaymentAdditionalData<'_, F>> for types::PaymentsSyncData
fn try_from(additional_data: PaymentAdditionalData<'_, F>) -> Result<Self, Self::Error> {
let payment_data = additional_data.payment_data;
let amount = payment_data
.surcharge_details
.as_ref()
.map(|surcharge_details| surcharge_details.final_amount)
.unwrap_or(payment_data.amount.into());
Ok(Self {
amount,
integrity_object: None,
mandate_id: payment_data.mandate_id.clone(),
connector_transaction_id: match payment_data.payment_attempt.connector_transaction_id {
Some(connector_txn_id) => {

View File

@ -209,6 +209,7 @@ pub async fn construct_payout_router_data<'a, F>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -371,6 +372,7 @@ pub async fn construct_refund_router_data<'a, F>(
refund_id: Some(refund.refund_id.clone()),
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
@ -608,6 +610,7 @@ pub async fn construct_accept_dispute_router_data<'a>(
dispute_id: Some(dispute.dispute_id.clone()),
refund_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -703,6 +706,7 @@ pub async fn construct_submit_evidence_router_data<'a>(
refund_id: None,
dispute_id: Some(dispute.dispute_id.clone()),
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -804,6 +808,7 @@ pub async fn construct_upload_file_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -902,6 +907,7 @@ pub async fn construct_defend_dispute_router_data<'a>(
refund_id: None,
dispute_id: Some(dispute.dispute_id.clone()),
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}
@ -989,6 +995,7 @@ pub async fn construct_retrieve_file_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}

View File

@ -119,6 +119,7 @@ pub async fn construct_webhook_router_data<'a>(
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
};
Ok(router_data)
}

View File

@ -131,3 +131,6 @@ counter_metric!(ACCESS_TOKEN_CACHE_HIT, GLOBAL_METER);
// A counter to indicate the access token cache miss
counter_metric!(ACCESS_TOKEN_CACHE_MISS, GLOBAL_METER);
// A counter to indicate the integrity check failures
counter_metric!(INTEGRITY_CHECK_FAILED, GLOBAL_METER);

View File

@ -71,6 +71,7 @@ fn get_default_router_data<F, Req, Resp>(
connector_response: None,
payment_method_status: None,
minor_amount_captured: None,
integrity_check: Ok(()),
}
}

View File

@ -15,7 +15,6 @@ pub mod pm_auth;
pub mod storage;
pub mod transformers;
use std::marker::PhantomData;
pub use api_models::{enums::Connector, mandates};
@ -790,6 +789,7 @@ impl ForeignFrom<&SetupMandateRouterData> for PaymentsAuthorizeData {
authentication_data: None,
customer_acceptance: data.request.customer_acceptance.clone(),
charges: None, // TODO: allow charges on mandates?
integrity_object: None,
}
}
}
@ -843,6 +843,7 @@ impl<F1, F2, T1, T2> ForeignFrom<(&RouterData<F1, T1, PaymentsResponseData>, T2)
dispute_id: data.dispute_id.clone(),
refund_id: data.refund_id.clone(),
connector_response: data.connector_response.clone(),
integrity_check: Ok(()),
}
}
}
@ -904,6 +905,7 @@ impl<F1, F2>
refund_id: None,
dispute_id: None,
connector_response: data.connector_response.clone(),
integrity_check: Ok(()),
}
}
}

View File

@ -55,6 +55,7 @@ impl VerifyConnectorData {
authentication_data: None,
customer_acceptance: None,
charges: None,
integrity_object: None,
}
}
@ -110,6 +111,7 @@ impl VerifyConnectorData {
refund_id: None,
dispute_id: None,
connector_response: None,
integrity_check: Ok(()),
}
}
}