Bug: feat(analytics): add issuer as filter for authentication analytics



Add `acs_reference_number` as filter for authentication analytics.
diff --git a/crates/analytics/src/auth_events/core.rs b/crates/analytics/src/auth_events/core.rs
index bdcbdd86a94..4d11f19d531 100644
--- a/crates/analytics/src/auth_events/core.rs
+++ b/crates/analytics/src/auth_events/core.rs
@@ -169,6 +169,7 @@ pub async fn get_filters(
             AuthEventDimensions::ErrorMessage => fil.error_message,
             AuthEventDimensions::AuthenticationConnector => fil.authentication_connector.map(|i| i.as_ref().to_string()),
             AuthEventDimensions::MessageVersion => fil.message_version,
+            AuthEventDimensions::AcsReferenceNumber => fil.acs_reference_number,
         })
         .collect::<Vec<String>>();
         res.query_data.push(AuthEventFilterValue {
diff --git a/crates/analytics/src/auth_events/filters.rs b/crates/analytics/src/auth_events/filters.rs
index 5f65eeaa279..1961116b84c 100644
--- a/crates/analytics/src/auth_events/filters.rs
+++ b/crates/analytics/src/auth_events/filters.rs
@@ -59,4 +59,5 @@ pub struct AuthEventFilterRow {
     pub error_message: Option<String>,
     pub authentication_connector: Option<DBEnumWrapper<AuthenticationConnectors>>,
     pub message_version: Option<String>,
+    pub acs_reference_number: Option<String>,
 }
diff --git a/crates/analytics/src/auth_events/metrics.rs b/crates/analytics/src/auth_events/metrics.rs
index a3d0fe7b683..6315c1338ee 100644
--- a/crates/analytics/src/auth_events/metrics.rs
+++ b/crates/analytics/src/auth_events/metrics.rs
@@ -45,6 +45,7 @@ pub struct AuthEventMetricRow {
     pub error_message: Option<String>,
     pub authentication_connector: Option<DBEnumWrapper<storage_enums::AuthenticationConnectors>>,
     pub message_version: Option<String>,
+    pub acs_reference_number: Option<String>,
     #[serde(with = "common_utils::custom_serde::iso8601::option")]
     pub start_bucket: Option<PrimitiveDateTime>,
     #[serde(with = "common_utils::custom_serde::iso8601::option")]
diff --git a/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs
index b82d1ef4c35..fbdc4b75b3e 100644
--- a/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs
+++ b/crates/analytics/src/auth_events/metrics/authentication_attempt_count.rs
@@ -110,6 +110,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/authentication_count.rs b/crates/analytics/src/auth_events/metrics/authentication_count.rs
index 682e8a07f6b..b4c0a5c5957 100644
--- a/crates/analytics/src/auth_events/metrics/authentication_count.rs
+++ b/crates/analytics/src/auth_events/metrics/authentication_count.rs
@@ -100,6 +100,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/authentication_error_message.rs b/crates/analytics/src/auth_events/metrics/authentication_error_message.rs
index 047511d477c..b064cb7ab30 100644
--- a/crates/analytics/src/auth_events/metrics/authentication_error_message.rs
+++ b/crates/analytics/src/auth_events/metrics/authentication_error_message.rs
@@ -119,6 +119,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/authentication_funnel.rs b/crates/analytics/src/auth_events/metrics/authentication_funnel.rs
index 9342e9047e0..3a122fd42a9 100644
--- a/crates/analytics/src/auth_events/metrics/authentication_funnel.rs
+++ b/crates/analytics/src/auth_events/metrics/authentication_funnel.rs
@@ -111,6 +111,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/authentication_success_count.rs b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs
index 984350efe6b..37435123623 100644
--- a/crates/analytics/src/auth_events/metrics/authentication_success_count.rs
+++ b/crates/analytics/src/auth_events/metrics/authentication_success_count.rs
@@ -105,6 +105,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs
index 5a2a9400b20..0265fc7e457 100644
--- a/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs
+++ b/crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs
@@ -115,6 +115,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs
index 279844388f2..178dcba20bf 100644
--- a/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs
+++ b/crates/analytics/src/auth_events/metrics/challenge_flow_count.rs
@@ -107,6 +107,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/challenge_success_count.rs b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs
index 91e8cc54289..b78f84326de 100644
--- a/crates/analytics/src/auth_events/metrics/challenge_success_count.rs
+++ b/crates/analytics/src/auth_events/metrics/challenge_success_count.rs
@@ -112,6 +112,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs
index b08edb10113..d114e36f140 100644
--- a/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs
+++ b/crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs
@@ -108,6 +108,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs b/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs
index 80417ac64bf..a56d5b660aa 100644
--- a/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs
+++ b/crates/analytics/src/auth_events/metrics/frictionless_success_count.rs
@@ -112,6 +112,7 @@ where
                         i.error_message.clone(),
                         i.authentication_connector.as_ref().map(|i| i.0),
                         i.message_version.clone(),
+                        i.acs_reference_number.clone(),
                         TimeRange {
                             start_time: match (granularity, i.start_bucket) {
                                 (Some(g), Some(st)) => g.clip_to_start(st)?,
diff --git a/crates/analytics/src/auth_events/types.rs b/crates/analytics/src/auth_events/types.rs
index ac7ee2462ee..863e50a0af2 100644
--- a/crates/analytics/src/auth_events/types.rs
+++ b/crates/analytics/src/auth_events/types.rs
@@ -53,6 +53,15 @@ where
                 )
                 .attach_printable("Error adding message version filter")?;
         }
+
+        if !self.acs_reference_number.is_empty() {
+            builder
+                .add_filter_in_range_clause(
+                    AuthEventDimensions::AcsReferenceNumber,
+                    &self.acs_reference_number,
+                )
+                .attach_printable("Error adding acs reference number filter")?;
+        }
         Ok(())
     }
 }
diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs
index 6a6237e6783..5a2f097bb0d 100644
--- a/crates/analytics/src/sqlx.rs
+++ b/crates/analytics/src/sqlx.rs
@@ -231,6 +231,11 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::metrics::AuthEventMetricRow
                 ColumnNotFound(_) => Ok(Default::default()),
                 e => Err(e),
             })?;
+        let acs_reference_number: Option<String> =
+            row.try_get("acs_reference_number").or_else(|e| match e {
+                ColumnNotFound(_) => Ok(Default::default()),
+                e => Err(e),
+            })?;
         let count: Option<i64> = row.try_get("count").or_else(|e| match e {
             ColumnNotFound(_) => Ok(Default::default()),
             e => Err(e),
@@ -249,6 +254,7 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::metrics::AuthEventMetricRow
             error_message,
             authentication_connector,
             message_version,
+            acs_reference_number,
             count,
             start_bucket,
             end_bucket,
@@ -288,6 +294,11 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::filters::AuthEventFilterRow
                 ColumnNotFound(_) => Ok(Default::default()),
                 e => Err(e),
             })?;
+        let acs_reference_number: Option<String> =
+            row.try_get("acs_reference_number").or_else(|e| match e {
+                ColumnNotFound(_) => Ok(Default::default()),
+                e => Err(e),
+            })?;
         Ok(Self {
             authentication_status,
             trans_status,
@@ -295,6 +306,7 @@ impl<'a> FromRow<'a, PgRow> for super::auth_events::filters::AuthEventFilterRow
             error_message,
             authentication_connector,
             message_version,
+            acs_reference_number,
         })
     }
 }
diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs
index a0ddead1363..0832788e261 100644
--- a/crates/analytics/src/utils.rs
+++ b/crates/analytics/src/utils.rs
@@ -51,6 +51,7 @@ pub fn get_auth_event_dimensions() -> Vec<NameDescription> {
     vec![
         AuthEventDimensions::AuthenticationConnector,
         AuthEventDimensions::MessageVersion,
+        AuthEventDimensions::AcsReferenceNumber,
     ]
     .into_iter()
     .map(Into::into)
diff --git a/crates/api_models/src/analytics/auth_events.rs b/crates/api_models/src/analytics/auth_events.rs
index 2360e564642..8a1c168aa2f 100644
--- a/crates/api_models/src/analytics/auth_events.rs
+++ b/crates/api_models/src/analytics/auth_events.rs
@@ -23,6 +23,8 @@ pub struct AuthEventFilters {
     pub authentication_connector: Vec<AuthenticationConnectors>,
     #[serde(default)]
     pub message_version: Vec<String>,
+    #[serde(default)]
+    pub acs_reference_number: Vec<String>,
 }
 
 #[derive(
@@ -50,6 +52,7 @@ pub enum AuthEventDimensions {
     ErrorMessage,
     AuthenticationConnector,
     MessageVersion,
+    AcsReferenceNumber,
 }
 
 #[derive(
@@ -135,6 +138,7 @@ pub struct AuthEventMetricsBucketIdentifier {
     pub error_message: Option<String>,
     pub authentication_connector: Option<AuthenticationConnectors>,
     pub message_version: Option<String>,
+    pub acs_reference_number: Option<String>,
     #[serde(rename = "time_range")]
     pub time_bucket: TimeRange,
     #[serde(rename = "time_bucket")]
@@ -151,6 +155,7 @@ impl AuthEventMetricsBucketIdentifier {
         error_message: Option<String>,
         authentication_connector: Option<AuthenticationConnectors>,
         message_version: Option<String>,
+        acs_reference_number: Option<String>,
         normalized_time_range: TimeRange,
     ) -> Self {
         Self {
@@ -160,6 +165,7 @@ impl AuthEventMetricsBucketIdentifier {
             error_message,
             authentication_connector,
             message_version,
+            acs_reference_number,
             time_bucket: normalized_time_range,
             start_time: normalized_time_range.start_time,
         }
@@ -173,6 +179,7 @@ impl Hash for AuthEventMetricsBucketIdentifier {
         self.authentication_type.hash(state);
         self.authentication_connector.hash(state);
         self.message_version.hash(state);
+        self.acs_reference_number.hash(state);
         self.error_message.hash(state);
         self.time_bucket.hash(state);
     }

## 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 -->
Added `acs_reference_number` as filter for authentication analytics.
Will display it as `issuer` on FE 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).
-->
Better insights into authentication data by applying filters

## 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)?
-->
Get Info curl:
```bash
curl --location 'http://localhost:8080/analytics/v1/auth_events/info' \
--header 'Accept: */*' \
--header 'Accept-Language: en-US,en;q=0.9' \
--header 'Connection: keep-alive' \
--header 'Content-Type: application/json' \
--header 'Referer: http://localhost:9000/dashboard/analytics-payments' \
--header 'Sec-Fetch-Dest: empty' \
--header 'Sec-Fetch-Mode: cors' \
--header 'Sec-Fetch-Site: same-origin' \
--header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36' \
--header 'X-Merchant-Id: merchant_1740414909' \
--header 'X-Profile-Id: pro_q9g6emqpc7b41Lo7UXpx' \
--header 'api-key: dev_zKT6VT7TvcxCtwq8N716HELM6HZJAwZvkWVHrlKAU54Pzu6D18GL3u3UswnkMkug' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiYWFiNWIxNDEtNjQwOC00YTUyLTk2MjMtNTVhNTgxMTU1M2U4IiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzQwNDE0OTA5Iiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTc0MTI3NDY4NSwib3JnX2lkIjoib3JnX1QwSEtYNHYyRGFRT2lHUDVwRk52IiwicHJvZmlsZV9pZCI6InByb19xOWc2ZW1xcGM3YjQxTG83VVhweCIsInRlbmFudF9pZCI6InB1YmxpYyJ9.LIExs1jjG6N5AFu5_S3oiuy77fWF0IbJmNGbK8HHLXI' \
--header 'sec-ch-ua: "Not(A:Brand";v="99", "Google Chrome";v="133", "Chromium";v="133"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"'
```

Sample Output:
```json
{
    "metrics": [
        {
            "name": "authentication_count",
            "desc": ""
        },
        {
            "name": "authentication_attempt_count",
            "desc": ""
        },
        {
            "name": "authentication_success_count",
            "desc": ""
        },
        {
            "name": "challenge_flow_count",
            "desc": ""
        },
        {
            "name": "frictionless_flow_count",
            "desc": ""
        },
        {
            "name": "frictionless_success_count",
            "desc": ""
        },
        {
            "name": "challenge_attempt_count",
            "desc": ""
        },
        {
            "name": "challenge_success_count",
            "desc": ""
        },
        {
            "name": "authentication_error_message",
            "desc": ""
        },
        {
            "name": "authentication_funnel",
            "desc": ""
        }
    ],
    "downloadDimensions": null,
    "dimensions": [
        {
            "name": "authentication_connector",
            "desc": ""
        },
        {
            "name": "message_version",
            "desc": ""
        },
        {
            "name": "acs_reference_number",
            "desc": ""
        }
    ]
}
```
acs_reference_number is being sent in the dimensions list.

Filters API:

```bash
curl --location 'http://localhost:8080/analytics/v1/filters/auth_events' \
--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/128.0.0.0 Safari/537.36' \
--header 'api-key: hyperswitch' \
--header 'authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiM2RlMDFhYWYtOWVkMi00YTY5LTg3YWItNWU1YmI2ZWFiNGQ5IiwibWVyY2hhbnRfaWQiOiJtZXJjaGFudF8xNzQyMjEyMzQxIiwicm9sZV9pZCI6Im9yZ19hZG1pbiIsImV4cCI6MTc0MjM4NTE0Miwib3JnX2lkIjoib3JnX3pMQmRNTnpLaDZrNmhiT2tockdJIiwicHJvZmlsZV9pZCI6InByb19LWnhHZ21rUHFkMldQdXBFa2psWCIsInRlbmFudF9pZCI6InB1YmxpYyJ9.efMstkbz90nfMKju4PmdIedl_tN-FHW3aQKD44jOnxo' \
--header 'sec-ch-ua: "Chromium";v="128", "Not;A=Brand";v="24", "Google Chrome";v="128"' \
--header 'sec-ch-ua-mobile: ?0' \
--header 'sec-ch-ua-platform: "macOS"' \
--data '{
    "timeRange": {
        "startTime": "2025-02-01T18:30:00.000Z",
        "endTime": "2025-02-28T09:22:30.000Z"
    },
    "groupByNames": [
        "message_version",
        "authentication_connector",
        "acs_reference_number"

    ],
    "source": "BATCH",
    "delta": true
}'
```

Sample Output:
```json
{
    "queryData": [
        {
            "dimension": "message_version",
            "values": [
                "2.3.1"
            ]
        },
        {
            "dimension": "authentication_connector",
            "values": [
                "netcetera"
            ]
        },
        {
            "dimension": "acs_reference_number",
            "values": [
                "3DS_LOA_ACS_201_13579"
            ]
        }
    ]
}
```


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