Bug: feat(analytics): Add currency as dimension and filter for disputes



Should add currency as dimension and filter for disputes, required for Analytics V2 Dashboard.

diff --git a/crates/analytics/src/disputes/core.rs b/crates/analytics/src/disputes/core.rs
index 540a14104c1..c1dcbaf2b2f 100644
--- a/crates/analytics/src/disputes/core.rs
+++ b/crates/analytics/src/disputes/core.rs
@@ -204,6 +204,7 @@ pub async fn get_filters(
         .filter_map(|fil: DisputeFilterRow| match dim {
             DisputeDimensions::DisputeStage => fil.dispute_stage,
             DisputeDimensions::Connector => fil.connector,
+            DisputeDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()),
         })
         .collect::<Vec<String>>();
         res.query_data.push(DisputeFilterValue {
diff --git a/crates/analytics/src/disputes/filters.rs b/crates/analytics/src/disputes/filters.rs
index cd60b502257..9e4aa302644 100644
--- a/crates/analytics/src/disputes/filters.rs
+++ b/crates/analytics/src/disputes/filters.rs
@@ -1,12 +1,16 @@
 use api_models::analytics::{disputes::DisputeDimensions, Granularity, TimeRange};
 use common_utils::errors::ReportSwitchExt;
+use diesel_models::enums::Currency;
 use error_stack::ResultExt;
 use time::PrimitiveDateTime;
 
 use crate::{
     enums::AuthInfo,
     query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window},
-    types::{AnalyticsCollection, AnalyticsDataSource, FiltersError, FiltersResult, LoadRow},
+    types::{
+        AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult,
+        LoadRow,
+    },
 };
 pub trait DisputeFilterAnalytics: LoadRow<DisputeFilterRow> {}
 
@@ -48,4 +52,5 @@ pub struct DisputeFilterRow {
     pub dispute_status: Option<String>,
     pub connector_status: Option<String>,
     pub dispute_stage: Option<String>,
+    pub currency: Option<DBEnumWrapper<Currency>>,
 }
diff --git a/crates/analytics/src/disputes/metrics.rs b/crates/analytics/src/disputes/metrics.rs
index 6514e5fcbe0..72bb09003d3 100644
--- a/crates/analytics/src/disputes/metrics.rs
+++ b/crates/analytics/src/disputes/metrics.rs
@@ -27,6 +27,7 @@ pub struct DisputeMetricRow {
     pub dispute_stage: Option<DBEnumWrapper<storage_enums::DisputeStage>>,
     pub dispute_status: Option<DBEnumWrapper<storage_enums::DisputeStatus>>,
     pub connector: Option<String>,
+    pub currency: Option<DBEnumWrapper<storage_enums::Currency>>,
     pub total: Option<bigdecimal::BigDecimal>,
     pub count: Option<i64>,
     #[serde(with = "common_utils::custom_serde::iso8601::option")]
diff --git a/crates/analytics/src/disputes/metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs
index ce962e284f6..b23dba4af38 100644
--- a/crates/analytics/src/disputes/metrics/dispute_status_metric.rs
+++ b/crates/analytics/src/disputes/metrics/dispute_status_metric.rs
@@ -97,6 +97,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs
index 9a7b0535819..0d54d75aee5 100644
--- a/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs
+++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/dispute_status_metric.rs
@@ -97,6 +97,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs
index 5c5eceb0619..bf2332ab12f 100644
--- a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs
+++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_amount_disputed.rs
@@ -98,6 +98,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs
index d6308b09f33..5d10becbbe2 100644
--- a/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs
+++ b/crates/analytics/src/disputes/metrics/sessionized_metrics/total_dispute_lost_amount.rs
@@ -99,6 +99,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/metrics/total_amount_disputed.rs b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs
index 68c7fa6d166..fc85ee39b26 100644
--- a/crates/analytics/src/disputes/metrics/total_amount_disputed.rs
+++ b/crates/analytics/src/disputes/metrics/total_amount_disputed.rs
@@ -97,6 +97,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs
index d14d4982701..33228cbb58b 100644
--- a/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs
+++ b/crates/analytics/src/disputes/metrics/total_dispute_lost_amount.rs
@@ -98,6 +98,7 @@ where
                     DisputeMetricsBucketIdentifier::new(
                         i.dispute_stage.as_ref().map(|i| i.0),
                         i.connector.clone(),
+                        i.currency.as_ref().map(|i| i.0),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/disputes/types.rs b/crates/analytics/src/disputes/types.rs
index 762e8d27554..da66744aede 100644
--- a/crates/analytics/src/disputes/types.rs
+++ b/crates/analytics/src/disputes/types.rs
@@ -24,6 +24,12 @@ where
                 .attach_printable("Error adding dispute stage filter")?;
         }
 
+        if !self.currency.is_empty() {
+            builder
+                .add_filter_in_range_clause(DisputeDimensions::Currency, &self.currency)
+                .attach_printable("Error adding currency filter")?;
+        }
+
         Ok(())
     }
 }
diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs
index 2a94d528768..653d019adeb 100644
--- a/crates/analytics/src/sqlx.rs
+++ b/crates/analytics/src/sqlx.rs
@@ -933,11 +933,17 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::filters::DisputeFilterRow {
                 ColumnNotFound(_) => Ok(Default::default()),
                 e => Err(e),
             })?;
+        let currency: Option<DBEnumWrapper<Currency>> =
+            row.try_get("currency").or_else(|e| match e {
+                ColumnNotFound(_) => Ok(Default::default()),
+                e => Err(e),
+            })?;
         Ok(Self {
             dispute_stage,
             dispute_status,
             connector,
             connector_status,
+            currency,
         })
     }
 }
@@ -957,6 +963,11 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::metrics::DisputeMetricRow {
             ColumnNotFound(_) => Ok(Default::default()),
             e => Err(e),
         })?;
+        let currency: Option<DBEnumWrapper<Currency>> =
+            row.try_get("currency").or_else(|e| match e {
+                ColumnNotFound(_) => Ok(Default::default()),
+                e => Err(e),
+            })?;
         let total: Option<bigdecimal::BigDecimal> = row.try_get("total").or_else(|e| match e {
             ColumnNotFound(_) => Ok(Default::default()),
             e => Err(e),
@@ -976,6 +987,7 @@ impl<'a> FromRow<'a, PgRow> for super::disputes::metrics::DisputeMetricRow {
             dispute_stage,
             dispute_status,
             connector,
+            currency,
             total,
             count,
             start_bucket,
diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs
index e373704b87c..179edee0413 100644
--- a/crates/api_models/src/analytics/disputes.rs
+++ b/crates/api_models/src/analytics/disputes.rs
@@ -4,7 +4,7 @@ use std::{
 };
 
 use super::{ForexMetric, NameDescription, TimeRange};
-use crate::enums::DisputeStage;
+use crate::enums::{Currency, DisputeStage};
 
 #[derive(
     Clone,
@@ -58,6 +58,7 @@ pub enum DisputeDimensions {
     // Consult the Dashboard FE folks since these also affects the order of metrics on FE
     Connector,
     DisputeStage,
+    Currency,
 }
 
 impl From<DisputeDimensions> for NameDescription {
@@ -82,13 +83,17 @@ impl From<DisputeMetrics> for NameDescription {
 pub struct DisputeFilters {
     #[serde(default)]
     pub dispute_stage: Vec<DisputeStage>,
+    #[serde(default)]
     pub connector: Vec<String>,
+    #[serde(default)]
+    pub currency: Vec<Currency>,
 }
 
 #[derive(Debug, serde::Serialize, Eq)]
 pub struct DisputeMetricsBucketIdentifier {
     pub dispute_stage: Option<DisputeStage>,
     pub connector: Option<String>,
+    pub currency: Option<Currency>,
     #[serde(rename = "time_range")]
     pub time_bucket: TimeRange,
     #[serde(rename = "time_bucket")]
@@ -100,6 +105,7 @@ impl Hash for DisputeMetricsBucketIdentifier {
     fn hash<H: Hasher>(&self, state: &mut H) {
         self.dispute_stage.hash(state);
         self.connector.hash(state);
+        self.currency.hash(state);
         self.time_bucket.hash(state);
     }
 }
@@ -117,11 +123,13 @@ impl DisputeMetricsBucketIdentifier {
     pub fn new(
         dispute_stage: Option<DisputeStage>,
         connector: Option<String>,
+        currency: Option<Currency>,
         normalized_time_range: TimeRange,
     ) -> Self {
         Self {
             dispute_stage,
             connector,
+            currency,
             time_bucket: normalized_time_range,
             start_time: normalized_time_range.start_time,
         }

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

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

## Description
<!-- Describe your changes in detail -->
Added currency as dimension and filter for disputes, required for Analytics V2 Dashboard.

### 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).
-->
For better filtering related analytics

## 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)?
-->
Hit the curl:
```bash
curl --location 'http://localhost:8080/analytics/v1/org/metrics/disputes' \
--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 'QueryType: Chart' \
--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/129.0.0.0 Safari/537.36' \
--header 'api-key: dev_ssyiqBXK7Ou1HUQuH0Z80BsHuV5pojc0uWli4QbNXGM1f4pn7jCIyb9VFiHlyATQ' \
--header 'sec-ch-ua: "Google Chrome";v="129", "Not=A?Brand";v="8", "Chromium";v="129"' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiNmNkZTA0NzYtMTFlMi00NGE5LTlkMjUtOTA5M2QzNDQwZjhlIiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzM1MDQxMjkzIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTczNjM2MDQ5Mywib3JnX2lkIjoib3JnX2pwanI5TkFEWlhqSENNYTU5MmF3IiwicHJvZmlsZV9pZCI6InByb19QRHUydVA3aWNuM2lXY0I3V0VVSSIsInRlbmFudF9pZCI6InB1YmxpYyJ9.tCy_qvwHxdPO1yor-A6IpZKVpC8uWxFGOgMvS35tRJ4' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"' \
--data '[
    {
        "timeRange": {
            "startTime": "2024-10-16T18:30:00Z",
            "endTime": "2024-10-24T12:09:00Z"
        },
        "timeSeries": {
            "granularity": "G_ONEDAY"
        },
        "filters": {
            "currency": ["USD"]
        },
        "mode": "ORDER",
        "source": "BATCH",
        "metrics": [
            "dispute_status_metric"
        ]
    }
]'
```
`clickhouse` as the source (query)
![image](https://github.com/user-attachments/assets/24323eae-e2c4-4442-9fcb-2c6bbb331678)
`sqlx` as the source (query)
<img width="1289" alt="image" src="https://github.com/user-attachments/assets/b6f8df85-2a3c-4521-be68-70d48caf9db5" />

Currency filter is now getting added in the query while trying to get the data to calculate the metrics.
## 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
