fix(payments): update error handling for payment void v2 (#9595)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Ayush Anand
2025-10-07 13:01:14 +05:30
committed by GitHub
parent cc4eaed570
commit e2ed289f2d
6 changed files with 87 additions and 8 deletions

View File

@ -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.",

View File

@ -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<String>)]
pub return_url: Option<common_utils::types::Url>,
/// Error details for the payment
pub error: Option<ErrorDetails>,
}
#[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<String>,
/// The unified error code across all connectors.
/// This can be relied upon for taking decisions based on the error.
pub unified_code: Option<String>,

View File

@ -1855,10 +1855,11 @@ impl
{
fn get_payment_intent_update(
&self,
_payment_data: &payments::PaymentCancelData<router_flow_types::Void>,
payment_data: &payments::PaymentCancelData<router_flow_types::Void>,
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<router_flow_types::Void>,
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<router_flow_types::Void>,
) -> 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,
}
}
}

View File

@ -14,6 +14,7 @@ pub struct PaymentFlowData {
pub merchant_id: common_utils::id_type::MerchantId,
pub customer_id: Option<common_utils::id_type::CustomerId>,
pub connector_customer: Option<String>,
pub connector: String,
pub payment_id: String,
pub attempt_id: String,
pub status: common_enums::AttemptStatus,

View File

@ -219,6 +219,7 @@ impl<T, Req: Clone, Resp: Clone> RouterDataConversion<T, Req, Resp> 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<T, Req: Clone, Resp: Clone> RouterDataConversion<T, Req, Resp> for PaymentF
merchant_id,
customer_id,
connector_customer,
connector,
payment_id,
attempt_id,
status,
@ -300,6 +302,7 @@ impl<T, Req: Clone, Resp: Clone> RouterDataConversion<T, Req, Resp> 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;

View File

@ -1017,6 +1017,7 @@ pub async fn construct_cancel_router_data_v2<'a>(
request: types::PaymentsCancelData,
connector_request_reference_id: String,
customer_id: Option<common_utils::id_type::CustomerId>,
connector_id: &str,
header_payload: Option<hyperswitch_domain_models::payments::HeaderPayload>,
) -> 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(),