Bug: [BUG] (connector): [Netcetera] Fix struct fields for Netcetera Authentication Response



### Bug Description

current structure

NetceteraAuthenticationSuccessResponse{
    ....
    pub challenge_cancel: Option<String>,
    pub trans_status_reason: Option<String>,
    pub message_extension: Option<Vec<MessageExtensionAttribute>>,
}

Correct structure

NetceteraAuthenticationSuccessResponse{
    ....
    authentication_response: {
    pub trans_status_reason: Option<String>,
    },
   authentication_request: {
    pub message_extension: Option<String>,
    }
}


### Expected Behavior

When the struct is fixed in code, the expected behavior is to:

Receive message_extension and trans_status_reason fields in Netcetera’s authentication response and store them in the authentication table.

Receive the challenge_cancel field from the Netcetera notification payload and store it in the authentication table.



### Actual Behavior

Currently expected behaviour is not happening and mentioned fields are not getting stored in authentication table.

### 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!
diff --git a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs
index 2025d29d10d..f080a76b051 100644
--- a/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/cybersource/transformers.rs
@@ -1439,29 +1439,23 @@ impl
                 )
                 .ok();
                 let effective_authentication_type = authn_data.authentication_type.map(Into::into);
-                let network_score: Option<u32> =
-                    if ccard.card_network == Some(common_enums::CardNetwork::CartesBancaires) {
-                        match authn_data.message_extension.as_ref() {
-                            Some(secret) => {
-                                let exposed_value = secret.clone().expose();
-                                match serde_json::from_value::<Vec<MessageExtensionAttribute>>(
-                                    exposed_value,
-                                ) {
-                                    Ok(exts) => extract_score_id(&exts),
-                                    Err(err) => {
-                                        router_env::logger::error!(
-                                            "Failed to deserialize message_extension: {:?}",
-                                            err
-                                        );
-                                        None
-                                    }
-                                }
-                            }
-                            None => None,
-                        }
-                    } else {
-                        None
-                    };
+                let network_score = (ccard.card_network
+                    == Some(common_enums::CardNetwork::CartesBancaires))
+                .then_some(authn_data.message_extension.as_ref())
+                .flatten()
+                .map(|secret| secret.clone().expose())
+                .and_then(|exposed| {
+                    serde_json::from_value::<Vec<MessageExtensionAttribute>>(exposed)
+                        .map_err(|err| {
+                            router_env::logger::error!(
+                                "Failed to deserialize message_extension: {:?}",
+                                err
+                            );
+                        })
+                        .ok()
+                        .and_then(|exts| extract_score_id(&exts))
+                });
+
                 CybersourceConsumerAuthInformation {
                     pares_status,
                     ucaf_collection_indicator,
@@ -1566,29 +1560,23 @@ impl
                     date_time::DateFormat::YYYYMMDDHHmmss,
                 )
                 .ok();
-                let network_score: Option<u32> =
-                    if ccard.card_network == Some(common_enums::CardNetwork::CartesBancaires) {
-                        match authn_data.message_extension.as_ref() {
-                            Some(secret) => {
-                                let exposed_value = secret.clone().expose();
-                                match serde_json::from_value::<Vec<MessageExtensionAttribute>>(
-                                    exposed_value,
-                                ) {
-                                    Ok(exts) => extract_score_id(&exts),
-                                    Err(err) => {
-                                        router_env::logger::error!(
-                                            "Failed to deserialize message_extension: {:?}",
-                                            err
-                                        );
-                                        None
-                                    }
-                                }
-                            }
-                            None => None,
-                        }
-                    } else {
-                        None
-                    };
+                let network_score = (ccard.card_network
+                    == Some(common_enums::CardNetwork::CartesBancaires))
+                .then_some(authn_data.message_extension.as_ref())
+                .flatten()
+                .map(|secret| secret.clone().expose())
+                .and_then(|exposed| {
+                    serde_json::from_value::<Vec<MessageExtensionAttribute>>(exposed)
+                        .map_err(|err| {
+                            router_env::logger::error!(
+                                "Failed to deserialize message_extension: {:?}",
+                                err
+                            );
+                        })
+                        .ok()
+                        .and_then(|exts| extract_score_id(&exts))
+                });
+
                 CybersourceConsumerAuthInformation {
                     pares_status,
                     ucaf_collection_indicator,
@@ -1697,30 +1685,23 @@ impl
                     date_time::DateFormat::YYYYMMDDHHmmss,
                 )
                 .ok();
-                let network_score: Option<u32> = if token_data.card_network
-                    == Some(common_enums::CardNetwork::CartesBancaires)
-                {
-                    match authn_data.message_extension.as_ref() {
-                        Some(secret) => {
-                            let exposed_value = secret.clone().expose();
-                            match serde_json::from_value::<Vec<MessageExtensionAttribute>>(
-                                exposed_value,
-                            ) {
-                                Ok(exts) => extract_score_id(&exts),
-                                Err(err) => {
-                                    router_env::logger::error!(
-                                        "Failed to deserialize message_extension: {:?}",
-                                        err
-                                    );
-                                    None
-                                }
-                            }
-                        }
-                        None => None,
-                    }
-                } else {
-                    None
-                };
+                let network_score = (token_data.card_network
+                    == Some(common_enums::CardNetwork::CartesBancaires))
+                .then_some(authn_data.message_extension.as_ref())
+                .flatten()
+                .map(|secret| secret.clone().expose())
+                .and_then(|exposed| {
+                    serde_json::from_value::<Vec<MessageExtensionAttribute>>(exposed)
+                        .map_err(|err| {
+                            router_env::logger::error!(
+                                "Failed to deserialize message_extension: {:?}",
+                                err
+                            );
+                        })
+                        .ok()
+                        .and_then(|exts| extract_score_id(&exts))
+                });
+
                 CybersourceConsumerAuthInformation {
                     pares_status,
                     ucaf_collection_indicator,
diff --git a/crates/hyperswitch_connectors/src/connectors/netcetera.rs b/crates/hyperswitch_connectors/src/connectors/netcetera.rs
index a2745dc4b08..60977d9b69b 100644
--- a/crates/hyperswitch_connectors/src/connectors/netcetera.rs
+++ b/crates/hyperswitch_connectors/src/connectors/netcetera.rs
@@ -214,14 +214,27 @@ impl IncomingWebhook for Netcetera {
             .body
             .parse_struct("netcetera ResultsResponseData")
             .change_context(ConnectorError::WebhookBodyDecodingFailed)?;
+
+        let challenge_cancel = webhook_body
+            .results_request
+            .as_ref()
+            .and_then(|v| v.get("challengeCancel").and_then(|v| v.as_str()))
+            .map(|s| s.to_string());
+
+        let challenge_code_reason = webhook_body
+            .results_request
+            .as_ref()
+            .and_then(|v| v.get("transStatusReason").and_then(|v| v.as_str()))
+            .map(|s| s.to_string());
+
         Ok(ExternalAuthenticationPayload {
             trans_status: webhook_body
                 .trans_status
                 .unwrap_or(common_enums::TransactionStatus::InformationOnly),
             authentication_value: webhook_body.authentication_value,
             eci: webhook_body.eci,
-            challenge_cancel: webhook_body.challenge_cancel,
-            challenge_code_reason: webhook_body.trans_status_reason,
+            challenge_cancel,
+            challenge_code_reason,
         })
     }
 }
diff --git a/crates/hyperswitch_connectors/src/connectors/netcetera/transformers.rs b/crates/hyperswitch_connectors/src/connectors/netcetera/transformers.rs
index 01189f6b926..1b8e2a65057 100644
--- a/crates/hyperswitch_connectors/src/connectors/netcetera/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/netcetera/transformers.rs
@@ -165,6 +165,21 @@ impl
                     .and_then(|req| req.three_ds_requestor_challenge_ind.as_ref())
                     .and_then(|v| v.first().cloned());
 
+                let message_extension = response
+                    .authentication_request
+                    .as_ref()
+                    .and_then(|req| req.message_extension.as_ref())
+                    .and_then(|v| match serde_json::to_value(v) {
+                        Ok(val) => Some(Secret::new(val)),
+                        Err(e) => {
+                            router_env::logger::error!(
+                                "Failed to serialize message_extension: {:?}",
+                                e
+                            );
+                            None
+                        }
+                    });
+
                 Ok(AuthenticationResponseData::AuthNResponse {
                     authn_flow_type,
                     authentication_value: response.authentication_value,
@@ -173,20 +188,9 @@ impl
                     ds_trans_id: response.authentication_response.ds_trans_id,
                     eci: response.eci,
                     challenge_code,
-                    challenge_cancel: response.challenge_cancel,
-                    challenge_code_reason: response.trans_status_reason,
-                    message_extension: response.message_extension.and_then(|v| {
-                        match serde_json::to_value(&v) {
-                            Ok(val) => Some(Secret::new(val)),
-                            Err(e) => {
-                                router_env::logger::error!(
-                                    "Failed to serialize message_extension: {:?}",
-                                    e
-                                );
-                                None
-                            }
-                        }
-                    }),
+                    challenge_cancel: None, // Note - challenge_cancel field is recieved in the RReq and updated in DB during external_authentication_incoming_webhook_flow
+                    challenge_code_reason: response.authentication_response.trans_status_reason,
+                    message_extension,
                 })
             }
             NetceteraAuthenticationResponse::Error(error_response) => Err(ErrorResponse {
@@ -664,9 +668,6 @@ pub struct NetceteraAuthenticationSuccessResponse {
     pub authentication_response: AuthenticationResponse,
     #[serde(rename = "base64EncodedChallengeRequest")]
     pub encoded_challenge_request: Option<String>,
-    pub challenge_cancel: Option<String>,
-    pub trans_status_reason: Option<String>,
-    pub message_extension: Option<Vec<MessageExtensionAttribute>>,
 }
 
 #[derive(Debug, Deserialize, Serialize)]
@@ -680,6 +681,7 @@ pub struct NetceteraAuthenticationFailureResponse {
 pub struct AuthenticationRequest {
     #[serde(rename = "threeDSRequestorChallengeInd")]
     pub three_ds_requestor_challenge_ind: Option<Vec<String>>,
+    pub message_extension: Option<serde_json::Value>,
 }
 
 #[derive(Debug, Deserialize, Serialize)]
@@ -693,6 +695,7 @@ pub struct AuthenticationResponse {
     #[serde(rename = "dsTransID")]
     pub ds_trans_id: Option<String>,
     pub acs_signed_content: Option<String>,
+    pub trans_status_reason: Option<String>,
 }
 
 #[derive(Debug, Deserialize, Serialize, Clone)]
@@ -742,6 +745,4 @@ pub struct ResultsResponseData {
 
     /// Optional object containing error details if any errors occurred during the process.
     pub error_details: Option<NetceteraErrorDetails>,
-    pub challenge_cancel: Option<String>,
-    pub trans_status_reason: Option<String>,
 }

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [X] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description

Current structure

```
NetceteraAuthenticationSuccessResponse{
    ....
    pub challenge_cancel: Option<String>,
    pub trans_status_reason: Option<String>,
    pub message_extension: Option<Vec<MessageExtensionAttribute>>,
}

```
Correct structure

```
NetceteraAuthenticationSuccessResponse{
    ....
    authentication_response: {
    pub trans_status_reason: Option<String>,
    },
   authentication_request: {
    pub message_extension: Option<String>,
    }
}

```
This PR introduces the above fix in the struct.

Fixes summary:
1. Remove **challenge_cancel** field from NetceteraAuthenticationSuccessResponse
2. Move **trans_status_reason** inside AuthenticationResponse struct
3. Move **message_extension** inside AuthenticationRequest struct
4. Remove fields **challenge_cancel** and **challenge_code_reason** from ResultsResponseData, and consume **challenge_cancel** and **challenge_code_reason** from webhook's results_request field (RReq)."

For your reference, please check the official specification of these attribute in 3DSecureio docs —it's clearer than what Netcetera currently provides
https://docs.3dsecure.io/3dsv2/specification_220.html#attr-RReq-challengeCancel
https://docs.3dsecure.io/3dsv2/specification_220.html#attr-ARes-transStatusReason
https://docs.3dsecure.io/3dsv2/specification_220.html#attr-ARes-messageExtension

### Additional Changes

- [ ] 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`
4. `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)?
-->

With the code changes, the expected behaviour is to,
1. Receive message_extension and trans_status_reason fields in Netcetera’s authentication response and store them in the authentication table.
2. Receive the challenge_cancel field from the Netcetera notification payload and store it in the authentication table.

### Steps to verify field **trans_status_reason**:

1. Test Netcetera 3ds challenge flow via SDK.
2. Check in Authentication Table if **challenge_code_reason** is present. (trans_status_reason is mapped to challenge_code_reason in authentication table)

**Query**
`select challenge_code_reason from authentication where connector_authentication_id='82e5df02-d83f-4adf-b00b-d5c10c52d4cd';`
<img width="1074" height="516" alt="Screenshot 2025-08-11 at 5 38 31 PM" src="https://github.com/user-attachments/assets/ffe93636-1630-4d75-b310-e28e9cdcab8a" />


### Steps to verify field **challenge_cancel**:

1. Test Netcetera 3ds challenge flow via SDK.
2. Cancel the challenge from challenge iframe.
3. Check in Authentication Table if **challenge_cancel** is present. 
<img width="1319" height="585" alt="Screenshot 2025-08-11 at 5 40 31 PM" src="https://github.com/user-attachments/assets/612f21a1-2da1-472d-a844-d205024bc01f" />


**Query**
`select challenge_cancel from authentication where connector_authentication_id='b51b6977-059d-4a59-92c9-1f3d37b43563';`
<img width="1319" height="585" alt="Screenshot 2025-08-11 at 5 43 11 PM" src="https://github.com/user-attachments/assets/a441d352-4e7b-4c0a-ac82-09849dbcbc2f" />


Please note we cannot verify field **message_extension** in integ/sandbox, can only be tested in prod.


## 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
