From b83e044b7d8e4b50acd6f8a6cbc27b802514946c Mon Sep 17 00:00:00 2001 From: chikke srujan <121822803+srujanchikke@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:47:13 +0530 Subject: [PATCH] fix(connector): Add network error message support for payment connectors (#7760) Co-authored-by: Chikke Srujan Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- .../connectors/bankofamerica/transformers.rs | 18 +++- .../connectors/cybersource/transformers.rs | 29 +++++- .../src/connectors/globalpay/response.rs | 2 + .../src/connectors/globalpay/transformers.rs | 36 ++++++-- .../src/connectors/worldpay/response.rs | 7 ++ .../src/connectors/worldpay/transformers.rs | 22 +++-- crates/router/src/connector/stripe.rs | 90 +++++++++++-------- .../src/connector/stripe/transformers.rs | 16 +++- 8 files changed, 162 insertions(+), 58 deletions(-) diff --git a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs index f3da0c98eb..3a627fdfdf 100644 --- a/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/bankofamerica/transformers.rs @@ -1598,6 +1598,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(get_error_response( &info_response.error_information, + &info_response.processor_information, &info_response.risk_information, Some(status), http_code, @@ -1923,6 +1924,7 @@ impl Ok(Self { response: Err(get_error_response( &item.response.error_information, + &item.response.processor_information, &risk_info, Some(status), item.http_code, @@ -2140,6 +2142,7 @@ impl TryFrom> Err(get_error_response( &item.response.error_information, &None, + &None, None, item.http_code, item.response.id, @@ -2227,6 +2230,7 @@ impl TryFrom> details: None, }), &None, + &None, None, item.http_code, item.response.id.clone(), @@ -2235,6 +2239,7 @@ impl TryFrom> Err(get_error_response( &item.response.error_information, &None, + &None, None, item.http_code, item.response.id.clone(), @@ -2323,6 +2328,7 @@ pub struct AuthenticationErrorInformation { fn get_error_response( error_data: &Option, + processor_information: &Option, risk_information: &Option, attempt_status: Option, status_code: u16, @@ -2354,6 +2360,14 @@ fn get_error_response( .join(", ") }) }); + let network_decline_code = processor_information + .as_ref() + .and_then(|info| info.response_code.clone()); + let network_advice_code = processor_information.as_ref().and_then(|info| { + info.merchant_advice + .as_ref() + .and_then(|merchant_advice| merchant_advice.code_raw.clone()) + }); let reason = get_error_reason( error_data @@ -2377,8 +2391,8 @@ fn get_error_response( status_code, attempt_status, connector_transaction_id: Some(transaction_id.clone()), - network_advice_code: None, - network_decline_code: None, + network_advice_code, + network_decline_code, network_error_message: None, } } diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs index 0ae8ca2535..36ce87364a 100644 --- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs @@ -2629,6 +2629,15 @@ pub struct ClientProcessorInformation { network_transaction_id: Option, avs: Option, card_verification: Option, + merchant_advice: Option, + response_code: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct MerchantAdvice { + code: Option, + code_raw: Option, } #[derive(Debug, Clone, Deserialize, Serialize)] @@ -2675,6 +2684,7 @@ fn get_error_response_if_failure( if utils::is_payment_failure(status) { Some(get_error_response( &info_response.error_information, + &info_response.processor_information, &info_response.risk_information, Some(status), http_code, @@ -3178,6 +3188,7 @@ impl if utils::is_payment_failure(status) { let response = Err(get_error_response( &info_response.error_information, + &None, &risk_info, Some(status), item.http_code, @@ -3543,6 +3554,7 @@ impl pub struct CybersourceTransactionResponse { id: String, application_information: ApplicationInformation, + processor_information: Option, client_reference_information: Option, error_information: Option, } @@ -3583,6 +3595,7 @@ impl Ok(Self { response: Err(get_error_response( &item.response.error_information, + &item.response.processor_information, &risk_info, Some(status), item.http_code, @@ -3701,6 +3714,7 @@ impl TryFrom> Err(get_error_response( &item.response.error_information, &None, + &None, None, item.http_code, item.response.id.clone(), @@ -3756,6 +3770,7 @@ impl TryFrom> details: None, }), &None, + &None, None, item.http_code, item.response.id.clone(), @@ -3764,6 +3779,7 @@ impl TryFrom> Err(get_error_response( &item.response.error_information, &None, + &None, None, item.http_code, item.response.id.clone(), @@ -4097,6 +4113,7 @@ pub struct AuthenticationErrorInformation { pub fn get_error_response( error_data: &Option, + processor_information: &Option, risk_information: &Option, attempt_status: Option, status_code: u16, @@ -4128,6 +4145,14 @@ pub fn get_error_response( .join(", ") }) }); + let network_decline_code = processor_information + .as_ref() + .and_then(|info| info.response_code.clone()); + let network_advice_code = processor_information.as_ref().and_then(|info| { + info.merchant_advice + .as_ref() + .and_then(|merchant_advice| merchant_advice.code_raw.clone()) + }); let reason = get_error_reason( error_data @@ -4151,8 +4176,8 @@ pub fn get_error_response( status_code, attempt_status, connector_transaction_id: Some(transaction_id), - network_advice_code: None, - network_decline_code: None, + network_advice_code, + network_decline_code, network_error_message: None, } } diff --git a/crates/hyperswitch_connectors/src/connectors/globalpay/response.rs b/crates/hyperswitch_connectors/src/connectors/globalpay/response.rs index a36cdc2a53..f0f7851a41 100644 --- a/crates/hyperswitch_connectors/src/connectors/globalpay/response.rs +++ b/crates/hyperswitch_connectors/src/connectors/globalpay/response.rs @@ -134,6 +134,8 @@ pub struct PaymentMethod { /// Result message from the payment method provider corresponding to the result code. pub message: Option, /// Result code from the payment method provider. + /// If a card authorization declines, the payment_method result and message include more detail from the Issuer on why it was declined. + /// For example, 51 - INSUFFICIENT FUNDS. This is generated by the issuing bank, who will provide decline codes in the response back to the authorization platform. pub result: Option, } diff --git a/crates/hyperswitch_connectors/src/connectors/globalpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/globalpay/transformers.rs index 124b611d04..a7fd9abd2f 100644 --- a/crates/hyperswitch_connectors/src/connectors/globalpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/globalpay/transformers.rs @@ -19,7 +19,10 @@ use hyperswitch_domain_models::{ PaymentsSyncRouterData, RefreshTokenRouterData, RefundExecuteRouterData, RefundsRouterData, }, }; -use hyperswitch_interfaces::{consts::NO_ERROR_MESSAGE, errors}; +use hyperswitch_interfaces::{ + consts::{self, NO_ERROR_MESSAGE}, + errors, +}; use masking::{ExposeInterface, PeekInterface, Secret}; use rand::distributions::DistString; use serde::{Deserialize, Serialize}; @@ -279,6 +282,7 @@ fn get_payment_response( status: common_enums::AttemptStatus, response: GlobalpayPaymentsResponse, redirection_data: Option, + status_code: u16, ) -> Result> { let mandate_reference = response.payment_method.as_ref().and_then(|pm| { pm.card @@ -295,12 +299,33 @@ fn get_payment_response( common_enums::AttemptStatus::Failure => Err(Box::new(ErrorResponse { message: response .payment_method - .and_then(|pm| pm.message) + .as_ref() + .and_then(|payment_method| payment_method.message.clone()) .unwrap_or_else(|| NO_ERROR_MESSAGE.to_string()), - ..Default::default() + code: response + .payment_method + .as_ref() + .and_then(|payment_method| payment_method.result.clone()) + .unwrap_or_else(|| consts::NO_ERROR_CODE.to_string()), + reason: response + .payment_method + .as_ref() + .and_then(|payment_method| payment_method.message.clone()), + status_code, + attempt_status: Some(status), + connector_transaction_id: Some(response.id), + network_decline_code: response + .payment_method + .as_ref() + .and_then(|payment_method| payment_method.result.clone()), + network_advice_code: None, + network_error_message: response + .payment_method + .as_ref() + .and_then(|payment_method| payment_method.message.clone()), })), _ => Ok(PaymentsResponseData::TransactionResponse { - resource_id: ResponseId::ConnectorTransactionId(response.id), + resource_id: ResponseId::ConnectorTransactionId(response.id.clone()), redirection_data: Box::new(redirection_data), mandate_reference: Box::new(mandate_reference), connector_metadata: None, @@ -336,9 +361,10 @@ impl TryFrom>, pub fraud: Option, #[serde(rename = "threeDS")] pub three_ds: Option, + pub advice: Option, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Advice { + pub code: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs index c5c76f3fa0..8fd41ea203 100644 --- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs +++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs @@ -748,7 +748,13 @@ impl None, None, None, - Some((res.refusal_code.clone(), res.refusal_description.clone())), + Some(( + res.refusal_code.clone(), + res.refusal_description.clone(), + res.advice + .as_ref() + .and_then(|advice_code| advice_code.code.clone()), + )), ), WorldpayPaymentResponseFields::FraudHighRisk(_) => (None, None, None, None, None), }) @@ -790,16 +796,18 @@ impl network_decline_code: None, network_error_message: None, }), - (_, Some((code, message))) => Err(ErrorResponse { - code, + (_, Some((code, message, advice_code))) => Err(ErrorResponse { + code: code.clone(), message: message.clone(), - reason: Some(message), + reason: Some(message.clone()), status_code: router_data.http_code, attempt_status: Some(status), connector_transaction_id: optional_correlation_id, - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: advice_code, + // Access Worldpay returns a raw response code in the refusalCode field (if enabled) containing the unmodified response code received either directly from the card scheme for Worldpay-acquired transactions, or from third party acquirers. + // You can use raw response codes to inform your retry logic. A rawCode is only returned if specifically requested. + network_decline_code: Some(code), + network_error_message: Some(message), }), }; Ok(Self { diff --git a/crates/router/src/connector/stripe.rs b/crates/router/src/connector/stripe.rs index 23ee62f08f..704b01246f 100644 --- a/crates/router/src/connector/stripe.rs +++ b/crates/router/src/connector/stripe.rs @@ -128,9 +128,9 @@ impl ConnectorCommon for Stripe { reason: response.error.message, attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -324,6 +324,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -331,9 +332,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -455,6 +456,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -462,9 +464,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -611,6 +613,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -618,9 +621,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -784,6 +787,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -791,9 +795,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -952,6 +956,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -959,9 +964,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -1082,6 +1087,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -1089,9 +1095,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -1245,6 +1251,7 @@ impl response .error .decline_code + .clone() .map(|decline_code| { format!("message - {}, decline_code - {}", message, decline_code) }) @@ -1252,9 +1259,9 @@ impl }), attempt_status: None, connector_transaction_id: response.error.payment_intent.map(|pi| pi.id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response.error.network_advice_code, + network_decline_code: response.error.network_decline_code, + network_error_message: response.error.decline_code.or(response.error.advice_code), }) } } @@ -1417,6 +1424,7 @@ impl services::ConnectorIntegration, pub decline_code: Option, pub payment_intent: Option, + pub network_advice_code: Option, + pub network_decline_code: Option, + pub advice_code: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)] @@ -3719,6 +3722,7 @@ pub struct WebhookEventObjectData { pub evidence_details: Option, pub status: Option, pub metadata: Option, + pub last_payment_error: Option, } #[derive(Debug, Clone, Serialize, Deserialize, strum::Display)] @@ -4178,9 +4182,15 @@ impl ForeignTryFrom<(&Option, u16, String)> for types::PaymentsRes status_code: http_code, attempt_status: None, connector_transaction_id: Some(response_id), - network_advice_code: None, - network_decline_code: None, - network_error_message: None, + network_advice_code: response + .as_ref() + .and_then(|res| res.network_advice_code.clone()), + network_decline_code: response + .as_ref() + .and_then(|res| res.network_decline_code.clone()), + network_error_message: response + .as_ref() + .and_then(|res| res.decline_code.clone().or(res.advice_code.clone())), }) } }