Issue:
Bug: [BUG] fix(payments): add connector error details in response when payment void fails



### Bug Description

When a payment void fails at the connector level, the error was not being propagated back in the API response.
This change ensures connector error details are included in the response, so clients can handle failures appropriately.

### Expected Behavior

When a payment void fails at the connector level, the error was not being propagated back in the API response.
This change ensures connector error details are included in the response, so clients can handle failures appropriately.

### Actual Behavior

Currently error is not coming in payment void if failed at connector level.

### Steps To Reproduce

Provide an unambiguous set of steps to reproduce this bug. Include code or configuration to reproduce, if relevant.
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'


### Context For The Bug

_No response_

### Environment

Are you using hyperswitch hosted version? Yes/No
If yes, please provide the value of the `x-request-id` response header to help us debug your issue.

If not (or if building/running locally), please provide the following details:
1. Operating System or Linux distribution:
2. Rust version (output of `rustc --version`): ``
3. App version (output of `cargo r --features vergen -- --version`): ``


### Have you spent some time checking if this bug has been raised before?

- [x] I checked and didn't find a similar issue

### Have you read the Contributing Guidelines?

- [x] I have read the [Contributing Guidelines](https://github.com/juspay/hyperswitch/blob/main/docs/CONTRIBUTING.md)

### Are you willing to submit a PR?

Yes, I am willing to submit a PR!

Pull request description:
## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [x] Bugfix
- [ ] New feature
- [x] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Add error details when payment void is failed at connector level and update the status accordingly.

### Additional Changes

- [x] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Request:
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_01999518b93e7d92bf68807c23919ddd/cancel' \
--header 'X-Profile-Id: pro_qBLZYRTYcOoyu0qPg4Nn' \
--header 'X-Merchant-Id: cloth_seller_nG8IQZGXCrxMkMyNF6oL' \
--header 'Authorization: api-key=dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--data '{
    "cancellation_reason": "duplicat"
}'
```
Response:
```
{
    "id": "12345_pay_01999518b93e7d92bf68807c23919ddd",
    "status": "failed",
    "cancellation_reason": "duplicat",
    "amount": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 10000,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": 0
    },
    "customer_id": "12345_cus_019995055fdd795190c715dccfbcdef4",
    "connector": "stripe",
    "created": "2025-09-29T10:50:49.594Z",
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "attempts": null,
    "return_url": null,
    "error": {
        "code": "No error code",
        "message": "No error message",
        "reason": "Invalid cancellation_reason: must be one of duplicate, fraudulent, requested_by_customer, or abandoned",
        "unified_code": null,
        "unified_message": null,
        "network_advice_code": null,
        "network_decline_code": null,
        "network_error_message": null
    }
}
```

Request:
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_0199951c2f4274b3b5a2825b0849a279/cancel' \
--header 'X-Profile-Id: pro_qBLZYRTYcOoyu0qPg4Nn' \
--header 'X-Merchant-Id: cloth_seller_nG8IQZGXCrxMkMyNF6oL' \
--header 'Authorization: api-key=dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--data '{
    "cancellation_reason": "duplicate"
}'
```
Response:
```
{
    "id": "12345_pay_0199951c2f4274b3b5a2825b0849a279",
    "status": "cancelled",
    "cancellation_reason": "duplicate",
    "amount": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 10000,
        "amount_to_capture": null,
        "amount_capturable": 10000,
        "amount_captured": 0
    },
    "customer_id": "12345_cus_019995055fdd795190c715dccfbcdef4",
    "connector": "stripe",
    "created": "2025-09-29T10:54:36.366Z",
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "attempts": null,
    "return_url": null,
    "error": null
}
```


closes #9616 


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible


Testing:

Request:
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_01999518b93e7d92bf68807c23919ddd/cancel' \
--header 'X-Profile-Id: pro_qBLZYRTYcOoyu0qPg4Nn' \
--header 'X-Merchant-Id: cloth_seller_nG8IQZGXCrxMkMyNF6oL' \
--header 'Authorization: api-key=dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--data '{
    "cancellation_reason": "duplicat"
}'
```
Response:
```
{
    "id": "12345_pay_01999518b93e7d92bf68807c23919ddd",
    "status": "failed",
    "cancellation_reason": "duplicat",
    "amount": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 10000,
        "amount_to_capture": null,
        "amount_capturable": 0,
        "amount_captured": 0
    },
    "customer_id": "12345_cus_019995055fdd795190c715dccfbcdef4",
    "connector": "stripe",
    "created": "2025-09-29T10:50:49.594Z",
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "attempts": null,
    "return_url": null,
    "error": {
        "code": "No error code",
        "message": "No error message",
        "reason": "Invalid cancellation_reason: must be one of duplicate, fraudulent, requested_by_customer, or abandoned",
        "unified_code": null,
        "unified_message": null,
        "network_advice_code": null,
        "network_decline_code": null,
        "network_error_message": null
    }
}
```

Request:
```
curl --location 'http://localhost:8080/v2/payments/12345_pay_0199951c2f4274b3b5a2825b0849a279/cancel' \
--header 'X-Profile-Id: pro_qBLZYRTYcOoyu0qPg4Nn' \
--header 'X-Merchant-Id: cloth_seller_nG8IQZGXCrxMkMyNF6oL' \
--header 'Authorization: api-key=dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--header 'Content-Type: application/json' \
--header 'api-key: dev_JqT4NtSwFvhxcBGkuWyDTIHvC7tkwp63QoyATDSxZ5X6PzudKWtOkzbJJvwZuRl3' \
--data '{
    "cancellation_reason": "duplicate"
}'
```
Response:
```
{
    "id": "12345_pay_0199951c2f4274b3b5a2825b0849a279",
    "status": "cancelled",
    "cancellation_reason": "duplicate",
    "amount": {
        "order_amount": 10000,
        "currency": "USD",
        "shipping_cost": null,
        "order_tax_amount": null,
        "external_tax_calculation": "skip",
        "surcharge_calculation": "skip",
        "surcharge_amount": null,
        "tax_on_surcharge": null,
        "net_amount": 10000,
        "amount_to_capture": null,
        "amount_capturable": 10000,
        "amount_captured": 0
    },
    "customer_id": "12345_cus_019995055fdd795190c715dccfbcdef4",
    "connector": "stripe",
    "created": "2025-09-29T10:54:36.366Z",
    "payment_method_type": "card",
    "payment_method_subtype": "credit",
    "attempts": null,
    "return_url": null,
    "error": null
}
```


closes #9616 

Patch:
diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs
index 7f397d6c60e..229dd6c241b 100644
--- a/crates/api_models/src/payments.rs
+++ b/crates/api_models/src/payments.rs
@@ -4921,6 +4921,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)]
@@ -6189,6 +6192,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>,
diff --git a/crates/hyperswitch_domain_models/src/router_data.rs b/crates/hyperswitch_domain_models/src/router_data.rs
index ad778e9aa74..fd66a8e2894 100644
--- a/crates/hyperswitch_domain_models/src/router_data.rs
+++ b/crates/hyperswitch_domain_models/src/router_data.rs
@@ -1830,10 +1830,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(),
@@ -1845,10 +1846,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(),
+            },
         }
     }
 
@@ -1872,7 +1918,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,
+        }
     }
 }
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 40be24722a5..bf6aaac1acb 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<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,
diff --git a/crates/hyperswitch_interfaces/src/conversion_impls.rs b/crates/hyperswitch_interfaces/src/conversion_impls.rs
index 3d3219a33f8..6b327f98496 100644
--- a/crates/hyperswitch_interfaces/src/conversion_impls.rs
+++ b/crates/hyperswitch_interfaces/src/conversion_impls.rs
@@ -218,6 +218,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,
@@ -264,6 +265,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,
@@ -299,6 +301,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;
diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs
index fa84e149118..fd90ca248eb 100644
--- a/crates/router/src/core/payments/transformers.rs
+++ b/crates/router/src/core/payments/transformers.rs
@@ -1008,6 +1008,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<
@@ -1026,6 +1027,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
@@ -1140,6 +1142,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?;
@@ -2169,6 +2172,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,
@@ -2181,6 +2189,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
@@ -5972,6 +5981,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(),