Bug: [FEATURE] - Add TimeRange based search on GlobalSearch



### Feature Description

Global search be able to give out result based on a given time range and not just rely on the query string given as a parameter in the request

### Possible Implementation

`{
    "query": "usd",
    "filters": {   
        "status": [
            "succeeded"
        ]
    },
    "timeRange": {
        "startTime": "2024-07-28T18:30:00Z",
        "endTime": "2024-07-30T18:45:00Z"
    }
}`
Time Range should be expected in the above manner

### Have you spent some time checking if this feature request 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?

None
diff --git a/crates/analytics/src/opensearch.rs b/crates/analytics/src/opensearch.rs
index 1031c815448..11ea4e4f7ab 100644
--- a/crates/analytics/src/opensearch.rs
+++ b/crates/analytics/src/opensearch.rs
@@ -1,6 +1,7 @@
 use api_models::{
     analytics::search::SearchIndex,
     errors::types::{ApiError, ApiErrorResponse},
+    payments::TimeRange,
 };
 use aws_config::{self, meta::region::RegionProviderChain, Region};
 use common_utils::errors::{CustomResult, ErrorSwitch};
@@ -18,8 +19,9 @@ use opensearch::{
     },
     MsearchParts, OpenSearch, SearchParts,
 };
-use serde_json::{json, Value};
+use serde_json::{json, Map, Value};
 use storage_impl::errors::ApplicationError;
+use time::PrimitiveDateTime;
 
 use super::{health_check::HealthCheck, query::QueryResult, types::QueryExecutionError};
 use crate::query::QueryBuildingError;
@@ -40,6 +42,23 @@ pub struct OpenSearchIndexes {
     pub disputes: String,
 }
 
+#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, PartialEq, Eq, Hash)]
+pub struct OpensearchTimeRange {
+    #[serde(with = "common_utils::custom_serde::iso8601")]
+    pub gte: PrimitiveDateTime,
+    #[serde(default, with = "common_utils::custom_serde::iso8601::option")]
+    pub lte: Option<PrimitiveDateTime>,
+}
+
+impl From<TimeRange> for OpensearchTimeRange {
+    fn from(time_range: TimeRange) -> Self {
+        Self {
+            gte: time_range.start_time,
+            lte: time_range.end_time,
+        }
+    }
+}
+
 #[derive(Clone, Debug, serde::Deserialize)]
 pub struct OpenSearchConfig {
     host: String,
@@ -377,6 +396,7 @@ pub struct OpenSearchQueryBuilder {
     pub offset: Option<i64>,
     pub count: Option<i64>,
     pub filters: Vec<(String, Vec<String>)>,
+    pub time_range: Option<OpensearchTimeRange>,
 }
 
 impl OpenSearchQueryBuilder {
@@ -387,6 +407,7 @@ impl OpenSearchQueryBuilder {
             offset: Default::default(),
             count: Default::default(),
             filters: Default::default(),
+            time_range: Default::default(),
         }
     }
 
@@ -396,27 +417,110 @@ impl OpenSearchQueryBuilder {
         Ok(())
     }
 
+    pub fn set_time_range(&mut self, time_range: OpensearchTimeRange) -> QueryResult<()> {
+        self.time_range = Some(time_range);
+        Ok(())
+    }
+
     pub fn add_filter_clause(&mut self, lhs: String, rhs: Vec<String>) -> QueryResult<()> {
         self.filters.push((lhs, rhs));
         Ok(())
     }
 
+    pub fn get_status_field(&self, index: &SearchIndex) -> &str {
+        match index {
+            SearchIndex::Refunds => "refund_status.keyword",
+            SearchIndex::Disputes => "dispute_status.keyword",
+            _ => "status.keyword",
+        }
+    }
+
+    pub fn replace_status_field(&self, filters: &[Value], index: &SearchIndex) -> Vec<Value> {
+        filters
+            .iter()
+            .map(|filter| {
+                if let Some(terms) = filter.get("terms").and_then(|v| v.as_object()) {
+                    let mut new_filter = filter.clone();
+                    if let Some(new_terms) =
+                        new_filter.get_mut("terms").and_then(|v| v.as_object_mut())
+                    {
+                        let key = "status.keyword";
+                        if let Some(status_terms) = terms.get(key) {
+                            new_terms.remove(key);
+                            new_terms.insert(
+                                self.get_status_field(index).to_string(),
+                                status_terms.clone(),
+                            );
+                        }
+                    }
+                    new_filter
+                } else {
+                    filter.clone()
+                }
+            })
+            .collect()
+    }
+
+    /// # Panics
+    ///
+    /// This function will panic if:
+    ///
+    /// * The structure of the JSON query is not as expected (e.g., missing keys or incorrect types).
+    ///
+    /// Ensure that the input data and the structure of the query are valid and correctly handled.
     pub fn construct_payload(&self, indexes: &[SearchIndex]) -> QueryResult<Vec<Value>> {
-        let mut query =
-            vec![json!({"multi_match": {"type": "phrase", "query": self.query, "lenient": true}})];
+        let mut query_obj = Map::new();
+        let mut bool_obj = Map::new();
+        let mut filter_array = Vec::new();
+
+        filter_array.push(json!({
+            "multi_match": {
+                "type": "phrase",
+                "query": self.query,
+                "lenient": true
+            }
+        }));
 
         let mut filters = self
             .filters
             .iter()
-            .map(|(k, v)| json!({"terms" : {k : v}}))
+            .map(|(k, v)| json!({"terms": {k: v}}))
             .collect::<Vec<Value>>();
 
-        query.append(&mut filters);
+        filter_array.append(&mut filters);
+
+        if let Some(ref time_range) = self.time_range {
+            let range = json!(time_range);
+            filter_array.push(json!({
+                "range": {
+                    "timestamp": range
+                }
+            }));
+        }
+
+        bool_obj.insert("filter".to_string(), Value::Array(filter_array));
+        query_obj.insert("bool".to_string(), Value::Object(bool_obj));
+
+        let mut query = Map::new();
+        query.insert("query".to_string(), Value::Object(query_obj));
 
-        // TODO add index specific filters
         Ok(indexes
             .iter()
-            .map(|_index| json!({"query": {"bool": {"filter": query}}}))
+            .map(|index| {
+                let updated_query = query
+                    .get("query")
+                    .and_then(|q| q.get("bool"))
+                    .and_then(|b| b.get("filter"))
+                    .and_then(|f| f.as_array())
+                    .map(|filters| self.replace_status_field(filters, index))
+                    .unwrap_or_default();
+
+                let mut final_query = Map::new();
+                final_query.insert("bool".to_string(), json!({ "filter": updated_query }));
+
+                let payload = json!({ "query": Value::Object(final_query) });
+                payload
+            })
             .collect::<Vec<Value>>())
     }
 }
diff --git a/crates/analytics/src/search.rs b/crates/analytics/src/search.rs
index 12ef7fb8d96..95b7f204b97 100644
--- a/crates/analytics/src/search.rs
+++ b/crates/analytics/src/search.rs
@@ -97,6 +97,10 @@ pub async fn msearch_results(
         };
     };
 
+    if let Some(time_range) = req.time_range {
+        query_builder.set_time_range(time_range.into()).switch()?;
+    };
+
     let response_text: OpenMsearchOutput = client
         .execute(query_builder)
         .await
@@ -221,6 +225,11 @@ pub async fn search_results(
             }
         };
     };
+
+    if let Some(time_range) = search_req.time_range {
+        query_builder.set_time_range(time_range.into()).switch()?;
+    };
+
     query_builder
         .set_offset_n_count(search_req.offset, search_req.count)
         .switch()?;
diff --git a/crates/api_models/src/analytics/search.rs b/crates/api_models/src/analytics/search.rs
index f27af75936d..b962f60cae0 100644
--- a/crates/api_models/src/analytics/search.rs
+++ b/crates/api_models/src/analytics/search.rs
@@ -2,6 +2,8 @@ use common_utils::hashing::HashedString;
 use masking::WithType;
 use serde_json::Value;
 
+use crate::payments::TimeRange;
+
 #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
 pub struct SearchFilters {
     pub payment_method: Option<Vec<String>>,
@@ -26,6 +28,8 @@ pub struct GetGlobalSearchRequest {
     pub query: String,
     #[serde(default)]
     pub filters: Option<SearchFilters>,
+    #[serde(default)]
+    pub time_range: Option<TimeRange>,
 }
 
 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
@@ -36,6 +40,8 @@ pub struct GetSearchRequest {
     pub query: String,
     #[serde(default)]
     pub filters: Option<SearchFilters>,
+    #[serde(default)]
+    pub time_range: Option<TimeRange>,
 }
 
 #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]

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

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

## Description
<!-- Describe your changes in detail -->
## Task 1
The `status` field is different in different indexes:
- `status` in Payment Attempts and Payment Intents
- `refund_status` in Refunds
- `dispute_status` in Disputes

The correct mapping of status for each index is done, to get the search-results

## Task 2
Added `Time Range based search` for the OpenSearch query


### 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`
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).
-->
Giving a better search experience for the merchants through the dashboard global-search

## 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)?
-->
Tested through Postman curls:
## Task 1
- Make a payment
```bash
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_8z4jfpKL8jjyFFbnQ29mzfnGQYbtwi7CAE52JjUFpzro8ZC5iSK5NyFZkr4paZ6o' \
--data-raw '{
  "amount": 6540,
  "currency": "INR",
  "confirm": true,
  "capture_method": "automatic",
  "capture_on": "2022-09-10T10:11:12Z",
  "amount_to_capture": 6540,
  "customer_id": "StripeCustomer",
  "email": "test@test6.com",
  "name": "John Doe",
  "phone": "999999999",
  "phone_country_code": "+1",
  "description": "Its my first payment request",
  "authentication_type": "no_three_ds",
  "return_url": "https://google.com",
  "payment_method": "card",
  "payment_method_type": "credit",
  "payment_method_data": {
    "card": {
      "card_number": "4242424242424242",
      "card_exp_month": "10",
      "card_exp_year": "25",
      "card_holder_name": "joseph Doe",
      "card_cvc": "123"
    }
  },
  "billing": {
    "address": {
      "line1": "1467",
      "line2": "Harrison Street",
      "line3": "Harrison Street",
      "city": "San Fransico",
      "state": "California",
      "zip": "94122",
      "country": "US",
      "first_name": "joseph",
      "last_name": "Doe"
    },
    "phone": {
      "number": "8056594427",
      "country_code": "+91"
    }
  },
  "shipping": {
    "address": {
      "line1": "1467",
      "line2": "Harrison Street",
      "line3": "Harrison Street",
      "city": "San Fransico",
      "state": "California",
      "zip": "94122",
      "country": "US",
      "first_name": "joseph",
      "last_name": "Doe"
    },
    "phone": {
      "number": "8056594427",
      "country_code": "+91"
    }
  },
  "statement_descriptor_name": "joseph",
  "statement_descriptor_suffix": "JS",
  "feature_metadata": {
    "search_tags": ["cc", "dd"]
  },
  "metadata": {
    "data2": "camel",
    "new_customer": "true",
    "login_date": "2019-09-10T10:11:12Z"
  }
}'
```
- Hit the search API with `status` as `success`
This will return the results of `Refunds` as internally, the `status` field gets converted into `refund_status` and `success` is the `Successful operation` in `Refunds`
```bash
curl --location 'http://localhost:8080/analytics/v1/search' \
--header 'Accept: */*' \
--header 'Accept-Language: en-US,en;q=0.9' \
--header 'Connection: keep-alive' \
--header 'Content-Type: application/json' \
--header 'Origin: http://localhost:9000' \
--header 'Referer: http://localhost:9000/' \
--header 'Sec-Fetch-Dest: empty' \
--header 'Sec-Fetch-Mode: cors' \
--header 'Sec-Fetch-Site: same-site' \
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
--header 'api-key: hyperswitch' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYjc2ZDVmZjMtNGI1ZS00OTMyLTk1ZDItNmE3MTEyZWZiNTNlIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzIxODE1MjkwIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTcyMjY3NTU3MSwib3JnX2lkIjoib3JnX3VFNk1LbExxalU5MWlGUEVGck1rIn0.inysDehseXLwSvbWbDEVbL2q3oUJMm2h8okCDss1kGY' \
--header 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"' \
--data '{
    "query": "inr",
    "filters": {
        "status": [
            "success"
        ]
    }
}'
```
<img width="946" alt="image" src="https://github.com/user-attachments/assets/3db4d143-09d1-46b0-80d0-1c4cb52d3ae1">

## Task 2
- Make a payment
```bash
curl --location 'http://localhost:8080/payments' \
--header 'Content-Type: application/json' \
--header 'Accept: application/json' \
--header 'api-key: dev_8z4jfpKL8jjyFFbnQ29mzfnGQYbtwi7CAE52JjUFpzro8ZC5iSK5NyFZkr4paZ6o' \
--data-raw '{
  "amount": 6540,
  "currency": "INR",
  "confirm": true,
  "capture_method": "automatic",
  "capture_on": "2022-09-10T10:11:12Z",
  "amount_to_capture": 6540,
  "customer_id": "StripeCustomer",
  "email": "test@test6.com",
  "name": "John Doe",
  "phone": "999999999",
  "phone_country_code": "+1",
  "description": "Its my first payment request",
  "authentication_type": "no_three_ds",
  "return_url": "https://google.com",
  "payment_method": "card",
  "payment_method_type": "credit",
  "payment_method_data": {
    "card": {
      "card_number": "4242424242424242",
      "card_exp_month": "10",
      "card_exp_year": "25",
      "card_holder_name": "joseph Doe",
      "card_cvc": "123"
    }
  },
  "billing": {
    "address": {
      "line1": "1467",
      "line2": "Harrison Street",
      "line3": "Harrison Street",
      "city": "San Fransico",
      "state": "California",
      "zip": "94122",
      "country": "US",
      "first_name": "joseph",
      "last_name": "Doe"
    },
    "phone": {
      "number": "8056594427",
      "country_code": "+91"
    }
  },
  "shipping": {
    "address": {
      "line1": "1467",
      "line2": "Harrison Street",
      "line3": "Harrison Street",
      "city": "San Fransico",
      "state": "California",
      "zip": "94122",
      "country": "US",
      "first_name": "joseph",
      "last_name": "Doe"
    },
    "phone": {
      "number": "8056594427",
      "country_code": "+91"
    }
  },
  "statement_descriptor_name": "joseph",
  "statement_descriptor_suffix": "JS",
  "feature_metadata": {
    "search_tags": ["cc", "dd"]
  },
  "metadata": {
    "data2": "camel",
    "new_customer": "true",
    "login_date": "2019-09-10T10:11:12Z"
  }
}'
```
- Hit the Search API:
Able to search based on the `timeRange` included in the request body
```bash
curl --location 'http://localhost:8080/analytics/v1/search' \
--header 'Accept: */*' \
--header 'Accept-Language: en-US,en;q=0.9' \
--header 'Connection: keep-alive' \
--header 'Content-Type: application/json' \
--header 'Origin: http://localhost:9000' \
--header 'Referer: http://localhost:9000/' \
--header 'Sec-Fetch-Dest: empty' \
--header 'Sec-Fetch-Mode: cors' \
--header 'Sec-Fetch-Site: same-site' \
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' \
--header 'api-key: hyperswitch' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYjc2ZDVmZjMtNGI1ZS00OTMyLTk1ZDItNmE3MTEyZWZiNTNlIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzIxODE1MjkwIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTcyMjQxNTgyOSwib3JnX2lkIjoib3JnX3VFNk1LbExxalU5MWlGUEVGck1rIn0.GUmj5mbLgnYVanig6kwrU_gvLARQU4E68s438dvrhdc' \
--header 'sec-ch-ua: "Not/A)Brand";v="8", "Chromium";v="126", "Google Chrome";v="126"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"' \
--data-raw '{
    "query": "inr",
    "filters": {
        "customer_email": [
            "test@test6.com"
        ],
        "status": [
            "succeeded"
        ]
    },
    "timeRange": {
        "startTime": "2024-07-29T00:30:00Z",
        "endTime": "2024-07-31T18:45:00Z"
    }
}'
```
<img width="936" alt="image" src="https://github.com/user-attachments/assets/95045257-86ad-466f-82fe-4121b290d580">

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