Issue:
Bug: [BUG] worldpay DDC submission race conditions



### Bug Description

Race condition in Worldpay 3DS DDC flow causes `bodyDoesNotMatchSchema` errors. 8-second timeout in `WorldpayDDCForm` JS fires before legitimate DDC completion, sending `collectionReference` to wrong endpoint (`/3dsChallenges` vs `/3dsDeviceData`).

### Expected Behavior

DDC `collectionReference` sent to `/3dsDeviceData` endpoint only. Payment state transitions: `DeviceDataCollectionPending` → `AuthenticationPending` → `Charged`.

### Actual Behavior

Timeout fires → empty redirect → state advances to `AuthenticationPending` → late `collectionReference` sent to `/3dsChallenges` → Worldpay rejects with `$.collectionReference` validation error.

### Steps To Reproduce

1. Trigger Worldpay 3DS payment with DDC delay >8s
2. Observe timeout in `api.rs:build_redirection_form()` 
3. Check logs for missing `PaymentsStart` between redirects
4. Verify `bodyDoesNotMatchSchema` error

### Context For The Bug

**File:** `crates/router/src/services/api.rs`
**Line:** `window.setTimeout(submitCollectionReference, 8000);` in `WorldpayDDCForm`
**Impact:** Intermittent 3DS failures when DDC >8s

**Log Evidence:** 11.6s gap between DDC form serve and timeout redirect proves race condition.

### Environment

Worldpay connector, 3DS flow with DDC

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

---

**Fix Plan:**

**Client-side (`api.rs`):**
1. Increase timeout: `8000` → `10000`
2. Add `ddcProcessed` flag to prevent multiple submissions
3. Handle 3 cases: DDC within 10s (normal), DDC never returns (empty submission), DDC after 10s (ignore)

**Server-side (`worldpay.rs`):**
1. Accept empty `collectionReference` in `DeviceDataCollectionPending` state
2. Ignore late DDC submissions when already in `AuthenticationPending` state
3. Add validation to prevent sending `collectionReference` to `/3dsChallenges` endpoint


Pull request description:
## Type of Change
- [x] Bugfix

## Description

Fixes race condition in Worldpay 3DS Device Data Collection (DDC) flow causing `bodyDoesNotMatchSchema` errors.

**Changes made:**
- Extended DDC timeout from 8s to 10s in client-side JavaScript
- Added `ddcProcessed` flag to prevent duplicate submissions on client-side
- Reject `collectionReference` submissions when payment is in `AuthenticationPending` state
- Fixed CSS media query syntax (`@media (max-width: 600px)`)
- Added Cypress test case for both client and server-side race conditions for DDC submissions

### Additional Changes
- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema  
- [ ] This PR modifies application configuration/environment variables

## Motivation and Context

Race condition caused intermittent 3DS payment failures when DDC took > 8 seconds.

## How did you test it?

<details>
    <summary>1. DDC race case in isolation </summary>

<img width="797" height="156" alt="Screenshot 2025-07-24 at 12 02 50 PM" src="https://github.com/user-attachments/assets/70d14add-9742-4ad3-b72f-da97dae5b197" />

</details>

<details>
    <summary>2. All test cases for Worldpay</summary>

<img width="461" height="873" alt="Screenshot 2025-07-25 at 12 52 56 AM" src="https://github.com/user-attachments/assets/7458a378-c413-4c8a-9b0d-4c09b2705a41" />


Note
- PSync for WP fails due to 404 and WP sending back a HTML rather than a JSON response (https://github.com/juspay/hyperswitch/pull/8753)

</details>


## Checklist
- [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:
<details>
    <summary>1. DDC race case in isolation </summary>

<img width="797" height="156" alt="Screenshot 2025-07-24 at 12 02 50 PM" src="https://github.com/user-attachments/assets/70d14add-9742-4ad3-b72f-da97dae5b197" />

</details>

<details>
    <summary>2. All test cases for Worldpay</summary>

<img width="461" height="873" alt="Screenshot 2025-07-25 at 12 52 56 AM" src="https://github.com/user-attachments/assets/7458a378-c413-4c8a-9b0d-4c09b2705a41" />


Note
- PSync for WP fails due to 404 and WP sending back a HTML rather than a JSON response (https://github.com/juspay/hyperswitch/pull/8753)

</details>

Patch:
diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay.rs b/crates/hyperswitch_connectors/src/connectors/worldpay.rs
index 009a02f7ef5..fcb582830b2 100644
--- a/crates/hyperswitch_connectors/src/connectors/worldpay.rs
+++ b/crates/hyperswitch_connectors/src/connectors/worldpay.rs
@@ -798,7 +798,16 @@ impl ConnectorIntegration<CompleteAuthorize, CompleteAuthorizeData, PaymentsResp
             .ok_or(errors::ConnectorError::MissingConnectorTransactionID)?;
         let stage = match req.status {
             enums::AttemptStatus::DeviceDataCollectionPending => "3dsDeviceData".to_string(),
-            _ => "3dsChallenges".to_string(),
+            enums::AttemptStatus::AuthenticationPending => "3dsChallenges".to_string(),
+            _ => {
+                return Err(
+                    errors::ConnectorError::RequestEncodingFailedWithReason(format!(
+                        "Invalid payment status for complete authorize: {:?}",
+                        req.status
+                    ))
+                    .into(),
+                );
+            }
         };
         Ok(format!(
             "{}api/payments/{connector_payment_id}/{stage}",
diff --git a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs
index 14bcd0afaf5..3a2fe91b231 100644
--- a/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs
+++ b/crates/hyperswitch_connectors/src/connectors/worldpay/transformers.rs
@@ -883,7 +883,29 @@ impl TryFrom<&types::PaymentsCompleteAuthorizeRouterData> for WorldpayCompleteAu
             .as_ref()
             .and_then(|redirect_response| redirect_response.params.as_ref())
             .ok_or(errors::ConnectorError::ResponseDeserializationFailed)?;
-        serde_urlencoded::from_str::<Self>(params.peek())
-            .change_context(errors::ConnectorError::ResponseDeserializationFailed)
+
+        let parsed_request = serde_urlencoded::from_str::<Self>(params.peek())
+            .change_context(errors::ConnectorError::ResponseDeserializationFailed)?;
+
+        match item.status {
+            enums::AttemptStatus::DeviceDataCollectionPending => Ok(parsed_request),
+            enums::AttemptStatus::AuthenticationPending => {
+                if parsed_request.collection_reference.is_some() {
+                    return Err(errors::ConnectorError::InvalidDataFormat {
+                        field_name:
+                            "collection_reference not allowed in AuthenticationPending state",
+                    }
+                    .into());
+                }
+                Ok(parsed_request)
+            }
+            _ => Err(
+                errors::ConnectorError::RequestEncodingFailedWithReason(format!(
+                    "Invalid payment status for complete authorize: {:?}",
+                    item.status
+                ))
+                .into(),
+            ),
+        }
     }
 }
diff --git a/crates/router/src/services/api.rs b/crates/router/src/services/api.rs
index 9b6c091a577..f79e6d7f5e9 100644
--- a/crates/router/src/services/api.rs
+++ b/crates/router/src/services/api.rs
@@ -1181,7 +1181,7 @@ pub fn build_redirection_form(
                     #loader1 {
                         width: 500px,
                     }
-                    @media max-width: 600px {
+                    @media (max-width: 600px) {
                         #loader1 {
                             width: 200px
                         }
@@ -1748,7 +1748,7 @@ pub fn build_redirection_form(
                                 #loader1 {
                                     width: 500px;
                                 }
-                                @media max-width: 600px {
+                                @media (max-width: 600px) {
                                     #loader1 {
                                         width: 200px;
                                     }
@@ -1777,7 +1777,21 @@ pub fn build_redirection_form(
                     script {
                         (PreEscaped(format!(
                             r#"
+                                var ddcProcessed = false;
+                                var timeoutHandle = null;
+                                
                                 function submitCollectionReference(collectionReference) {{
+                                    if (ddcProcessed) {{
+                                        console.log("DDC already processed, ignoring duplicate submission");
+                                        return;
+                                    }}
+                                    ddcProcessed = true;
+                                    
+                                    if (timeoutHandle) {{
+                                        clearTimeout(timeoutHandle);
+                                        timeoutHandle = null;
+                                    }}
+                                    
                                     var redirectPathname = window.location.pathname.replace(/payments\/redirect\/([^\/]+)\/([^\/]+)\/[^\/]+/, "payments/$1/$2/redirect/complete/worldpay");
                                     var redirectUrl = window.location.origin + redirectPathname;
                                     try {{
@@ -1796,12 +1810,17 @@ pub fn build_redirection_form(
                                             window.location.replace(redirectUrl);
                                         }}
                                     }} catch (error) {{
+                                        console.error("Error submitting DDC:", error);
                                         window.location.replace(redirectUrl);
                                     }}
                                 }}
                                 var allowedHost = "{}";
                                 var collectionField = "{}";
                                 window.addEventListener("message", function(event) {{
+                                    if (ddcProcessed) {{
+                                        console.log("DDC already processed, ignoring message event");
+                                        return;
+                                    }}
                                     if (event.origin === allowedHost) {{
                                         try {{
                                             var data = JSON.parse(event.data);
@@ -1821,8 +1840,13 @@ pub fn build_redirection_form(
                                     submitCollectionReference("");
                                 }});
 
-                                // Redirect within 8 seconds if no collection reference is received
-                                window.setTimeout(submitCollectionReference, 8000);
+                                // Timeout after 10 seconds and will submit empty collection reference
+                                timeoutHandle = window.setTimeout(function() {{
+                                    if (!ddcProcessed) {{
+                                        console.log("DDC timeout reached, submitting empty collection reference");
+                                        submitCollectionReference("");
+                                    }}
+                                }}, 10000);
                             "#,
                             endpoint.host_str().map_or(endpoint.as_ref().split('/').take(3).collect::<Vec<&str>>().join("/"), |host| format!("{}://{}", endpoint.scheme(), host)),
                             collection_id.clone().unwrap_or("".to_string())))