diff --git a/api-reference/v2/openapi_spec_v2.json b/api-reference/v2/openapi_spec_v2.json index 50d307830b..9b196b6256 100644 --- a/api-reference/v2/openapi_spec_v2.json +++ b/api-reference/v2/openapi_spec_v2.json @@ -11060,6 +11060,11 @@ "type": "string", "description": "The error message" }, + "reason": { + "type": "string", + "description": "The detailed error reason that was returned by the connector.", + "nullable": true + }, "unified_code": { "type": "string", "description": "The unified error code across all connectors.\nThis can be relied upon for taking decisions based on the error.", diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 993fa3f077..7fcb045759 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4992,6 +4992,9 @@ pub struct PaymentsCancelResponse { /// The url to which user must be redirected to after completion of the purchase #[schema(value_type = Option)] pub return_url: Option, + + /// Error details for the payment + pub error: Option, } #[derive(Default, Clone, Debug, Eq, PartialEq, serde::Serialize)] @@ -6284,6 +6287,8 @@ pub struct ErrorDetails { pub code: String, /// The error message pub message: String, + /// The detailed error reason that was returned by the connector. + pub reason: Option, /// The unified error code across all connectors. /// This can be relied upon for taking decisions based on the error. pub unified_code: Option, diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs index 90ecd26a8e..0dcf6f86fe 100644 --- a/crates/hyperswitch_domain_models/src/router_data.rs +++ b/crates/hyperswitch_domain_models/src/router_data.rs @@ -1855,10 +1855,11 @@ impl { fn get_payment_intent_update( &self, - _payment_data: &payments::PaymentCancelData, + payment_data: &payments::PaymentCancelData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentIntentUpdate { - let intent_status = common_enums::IntentStatus::from(self.status); + let intent_status = + common_enums::IntentStatus::from(self.get_attempt_status_for_db_update(payment_data)); PaymentIntentUpdate::VoidUpdate { status: intent_status, updated_by: storage_scheme.to_string(), @@ -1870,10 +1871,55 @@ impl payment_data: &payments::PaymentCancelData, storage_scheme: common_enums::MerchantStorageScheme, ) -> PaymentAttemptUpdate { - PaymentAttemptUpdate::VoidUpdate { - status: self.status, - cancellation_reason: payment_data.payment_attempt.cancellation_reason.clone(), - updated_by: storage_scheme.to_string(), + match &self.response { + Err(ref error_response) => { + let ErrorResponse { + code, + message, + reason, + status_code: _, + attempt_status: _, + connector_transaction_id, + network_decline_code, + network_advice_code, + network_error_message, + connector_metadata: _, + } = error_response.clone(); + + // Handle errors exactly + let status = match error_response.attempt_status { + // Use the status sent by connector in error_response if it's present + Some(status) => status, + None => match error_response.status_code { + 500..=511 => common_enums::AttemptStatus::Pending, + _ => common_enums::AttemptStatus::VoidFailed, + }, + }; + + let error_details = ErrorDetails { + code, + message, + reason, + unified_code: None, + unified_message: None, + network_advice_code, + network_decline_code, + network_error_message, + }; + + PaymentAttemptUpdate::ErrorUpdate { + status, + amount_capturable: Some(MinorUnit::zero()), + error: error_details, + updated_by: storage_scheme.to_string(), + connector_payment_id: connector_transaction_id, + } + } + Ok(ref _response) => PaymentAttemptUpdate::VoidUpdate { + status: self.status, + cancellation_reason: payment_data.payment_attempt.cancellation_reason.clone(), + updated_by: storage_scheme.to_string(), + }, } } @@ -1897,7 +1943,16 @@ impl &self, _payment_data: &payments::PaymentCancelData, ) -> common_enums::AttemptStatus { - // For void operations, return Voided status - common_enums::AttemptStatus::Voided + // For void operations, determine status based on response + match &self.response { + Err(ref error_response) => match error_response.attempt_status { + Some(status) => status, + None => match error_response.status_code { + 500..=511 => common_enums::AttemptStatus::Pending, + _ => common_enums::AttemptStatus::VoidFailed, + }, + }, + Ok(ref _response) => self.status, + } } } diff --git a/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs b/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs index f012c62b8b..7280a2f3cc 100644 --- a/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs +++ b/crates/hyperswitch_domain_models/src/router_data_v2/flow_common_types.rs @@ -14,6 +14,7 @@ pub struct PaymentFlowData { pub merchant_id: common_utils::id_type::MerchantId, pub customer_id: Option, pub connector_customer: Option, + pub connector: String, pub payment_id: String, pub attempt_id: String, pub status: common_enums::AttemptStatus, diff --git a/crates/hyperswitch_interfaces/src/conversion_impls.rs b/crates/hyperswitch_interfaces/src/conversion_impls.rs index 9c5e420e86..eb394fca05 100644 --- a/crates/hyperswitch_interfaces/src/conversion_impls.rs +++ b/crates/hyperswitch_interfaces/src/conversion_impls.rs @@ -219,6 +219,7 @@ impl RouterDataConversion for PaymentF merchant_id: old_router_data.merchant_id.clone(), customer_id: old_router_data.customer_id.clone(), connector_customer: old_router_data.connector_customer.clone(), + connector: old_router_data.connector.clone(), payment_id: old_router_data.payment_id.clone(), attempt_id: old_router_data.attempt_id.clone(), status: old_router_data.status, @@ -265,6 +266,7 @@ impl RouterDataConversion for PaymentF merchant_id, customer_id, connector_customer, + connector, payment_id, attempt_id, status, @@ -300,6 +302,7 @@ impl RouterDataConversion for PaymentF router_data.merchant_id = merchant_id; router_data.customer_id = customer_id; router_data.connector_customer = connector_customer; + router_data.connector = connector; router_data.payment_id = payment_id; router_data.attempt_id = attempt_id; router_data.status = status; diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 8467a17e12..a9a87497b6 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -1017,6 +1017,7 @@ pub async fn construct_cancel_router_data_v2<'a>( request: types::PaymentsCancelData, connector_request_reference_id: String, customer_id: Option, + connector_id: &str, header_payload: Option, ) -> RouterResult< RouterDataV2< @@ -1035,6 +1036,7 @@ pub async fn construct_cancel_router_data_v2<'a>( merchant_id: merchant_account.get_id().clone(), customer_id, connector_customer: None, + connector: connector_id.to_owned(), payment_id: payment_data .payment_attempt .payment_id @@ -1149,6 +1151,7 @@ pub async fn construct_router_data_for_cancel<'a>( request, connector_request_reference_id.clone(), customer_id.clone(), + connector_id, header_payload.clone(), ) .await?; @@ -2183,6 +2186,11 @@ where .connector .as_ref() .and_then(|conn| api_enums::Connector::from_str(conn).ok()); + let error = payment_attempt + .error + .as_ref() + .map(api_models::payments::ErrorDetails::foreign_from); + let response = api_models::payments::PaymentsCancelResponse { id: payment_intent.id.clone(), status: payment_intent.status, @@ -2195,6 +2203,7 @@ where payment_method_subtype: Some(payment_attempt.payment_method_subtype), attempts: None, return_url: payment_intent.return_url.clone(), + error, }; let headers = connector_http_status_code @@ -6000,6 +6009,7 @@ impl ForeignFrom<&hyperswitch_domain_models::payments::payment_attempt::ErrorDet Self { code: error_details.code.to_owned(), message: error_details.message.to_owned(), + reason: error_details.reason.clone(), unified_code: error_details.unified_code.clone(), unified_message: error_details.unified_message.clone(), network_advice_code: error_details.network_advice_code.clone(),