// File: crates/analytics/src/frm/core.rs

#![allow(dead_code)]
use std::collections::HashMap;

use api_models::analytics::{
    frm::{FrmDimensions, FrmMetrics, FrmMetricsBucketIdentifier, FrmMetricsBucketResponse},
    AnalyticsMetadata, FrmFilterValue, FrmFiltersResponse, GetFrmFilterRequest,
    GetFrmMetricRequest, MetricsResponse,
};
use error_stack::ResultExt;
use router_env::{
    logger,
    tracing::{self, Instrument},
};

use super::{
    filters::{get_frm_filter_for_dimension, FrmFilterRow},
    FrmMetricsAccumulator,
};
use crate::{
    errors::{AnalyticsError, AnalyticsResult},
    frm::FrmMetricAccumulator,
    metrics, AnalyticsProvider,
};

pub async fn get_metrics(
    pool: &AnalyticsProvider,
    merchant_id: &common_utils::id_type::MerchantId,
    req: GetFrmMetricRequest,
) -> AnalyticsResult<MetricsResponse<FrmMetricsBucketResponse>> {
    let mut metrics_accumulator: HashMap<FrmMetricsBucketIdentifier, FrmMetricsAccumulator> =
        HashMap::new();
    let mut set = tokio::task::JoinSet::new();
    for metric_type in req.metrics.iter().cloned() {
        let req = req.clone();
        let pool = pool.clone();
        let task_span =
            tracing::debug_span!("analytics_frm_query", frm_metric = metric_type.as_ref());
        // Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference
        // We can optimize away this clone once that is fixed
        let merchant_id_scoped = merchant_id.to_owned();
        set.spawn(
            async move {
                let data = pool
                    .get_frm_metrics(
                        &metric_type,
                        &req.group_by_names.clone(),
                        &merchant_id_scoped,
                        &req.filters,
                        req.time_series.map(|t| t.granularity),
                        &req.time_range,
                    )
                    .await
                    .change_context(AnalyticsError::UnknownError);
                (metric_type, data)
            }
            .instrument(task_span),
        );
    }

    while let Some((metric, data)) = set
        .join_next()
        .await
        .transpose()
        .change_context(AnalyticsError::UnknownError)?
    {
        let data = data?;

        let attributes = router_env::metric_attributes!(
            ("metric_type", metric.to_string()),
            ("source", pool.to_string()),
        );
        let value = u64::try_from(data.len());
        if let Ok(val) = value {
            metrics::BUCKETS_FETCHED.record(val, attributes);
            logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val);
        }

        for (id, value) in data {
            logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}");
            let metrics_builder = metrics_accumulator.entry(id).or_default();
            match metric {
                FrmMetrics::FrmBlockedRate => {
                    metrics_builder.frm_blocked_rate.add_metrics_bucket(&value)
                }
                FrmMetrics::FrmTriggeredAttempts => metrics_builder
                    .frm_triggered_attempts
                    .add_metrics_bucket(&value),
            }
        }

        logger::debug!(
            "Analytics Accumulated Results: metric: {}, results: {:#?}",
            metric,
            metrics_accumulator
        );
    }
    let query_data: Vec<FrmMetricsBucketResponse> = metrics_accumulator
        .into_iter()
        .map(|(id, val)| FrmMetricsBucketResponse {
            values: val.collect(),
            dimensions: id,
        })
        .collect();

    Ok(MetricsResponse {
        query_data,
        meta_data: [AnalyticsMetadata {
            current_time_range: req.time_range,
        }],
    })
}

pub async fn get_filters(
    pool: &AnalyticsProvider,
    req: GetFrmFilterRequest,
    merchant_id: &common_utils::id_type::MerchantId,
) -> AnalyticsResult<FrmFiltersResponse> {
    let mut res = FrmFiltersResponse::default();
    for dim in req.group_by_names {
        let values = match pool {
            AnalyticsProvider::Sqlx(pool) => {
    get_frm_filter_for_dimension(dim, merchant_id, &req.time_range, pool)
        .await
}
            AnalyticsProvider::Clickhouse(pool) => {
    get_frm_filter_for_dimension(dim, merchant_id, &req.time_range, pool)
        .await
}
        AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) => {
    let ckh_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        ckh_pool,
    )
    .await;
    let sqlx_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        sqlx_pool,
    )
    .await;
    match (&sqlx_result, &ckh_result) {
        (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
            logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres frm analytics filters")
        },
        _ => {}
    };
    ckh_result
}
        AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => {
    let ckh_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        ckh_pool,
    )
    .await;
    let sqlx_result = get_frm_filter_for_dimension(
        dim,
        merchant_id,
        &req.time_range,
        sqlx_pool,
    )
    .await;
    match (&sqlx_result, &ckh_result) {
        (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
            logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres frm analytics filters")
        },
        _ => {}
    };
    sqlx_result
}
}
        .change_context(AnalyticsError::UnknownError)?
        .into_iter()
        .filter_map(|fil: FrmFilterRow| match dim {
            FrmDimensions::FrmStatus => fil.frm_status.map(|i| i.as_ref().to_string()),
            FrmDimensions::FrmName => fil.frm_name,
            FrmDimensions::FrmTransactionType => {
                fil.frm_transaction_type.map(|i| i.as_ref().to_string())
            }
        })
        .collect::<Vec<String>>();
        res.query_data.push(FrmFilterValue {
            dimension: dim,
            values,
        })
    }
    Ok(res)
}


// File: crates/analytics/src/frm/types.rs

use api_models::analytics::frm::{FrmDimensions, FrmFilters};
use error_stack::ResultExt;

use crate::{
    query::{QueryBuilder, QueryFilter, QueryResult, ToSql},
    types::{AnalyticsCollection, AnalyticsDataSource},
};

impl<T> QueryFilter<T> for FrmFilters
where
    T: AnalyticsDataSource,
    AnalyticsCollection: ToSql<T>,
{
    fn set_filter_clause(&self, builder: &mut QueryBuilder<T>) -> QueryResult<()> {
        if !self.frm_status.is_empty() {
            builder
                .add_filter_in_range_clause(FrmDimensions::FrmStatus, &self.frm_status)
                .attach_printable("Error adding frm status filter")?;
        }

        if !self.frm_name.is_empty() {
            builder
                .add_filter_in_range_clause(FrmDimensions::FrmName, &self.frm_name)
                .attach_printable("Error adding frm name filter")?;
        }

        if !self.frm_transaction_type.is_empty() {
            builder
                .add_filter_in_range_clause(
                    FrmDimensions::FrmTransactionType,
                    &self.frm_transaction_type,
                )
                .attach_printable("Error adding frm transaction type filter")?;
        }

        Ok(())
    }
}


// File: crates/analytics/src/frm/metrics.rs

use api_models::analytics::{
    frm::{FrmDimensions, FrmFilters, FrmMetrics, FrmMetricsBucketIdentifier, FrmTransactionType},
    Granularity, TimeRange,
};
use diesel_models::enums as storage_enums;
use time::PrimitiveDateTime;
mod frm_blocked_rate;
mod frm_triggered_attempts;

use frm_blocked_rate::FrmBlockedRate;
use frm_triggered_attempts::FrmTriggeredAttempts;

use crate::{
    query::{Aggregate, GroupByClause, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult},
};
#[derive(Debug, Eq, PartialEq, serde::Deserialize)]
pub struct FrmMetricRow {
    pub frm_name: Option<String>,
    pub frm_status: Option<DBEnumWrapper<storage_enums::FraudCheckStatus>>,
    pub frm_transaction_type: Option<DBEnumWrapper<FrmTransactionType>>,
    pub total: Option<bigdecimal::BigDecimal>,
    pub count: Option<i64>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub start_bucket: Option<PrimitiveDateTime>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub end_bucket: Option<PrimitiveDateTime>,
}

pub trait FrmMetricAnalytics: LoadRow<FrmMetricRow> {}

#[async_trait::async_trait]
pub trait FrmMetric<T>
where
    T: AnalyticsDataSource + FrmMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[FrmDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &FrmFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<Vec<(FrmMetricsBucketIdentifier, FrmMetricRow)>>;
}

#[async_trait::async_trait]
impl<T> FrmMetric<T> for FrmMetrics
where
    T: AnalyticsDataSource + FrmMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[FrmDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &FrmFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<Vec<(FrmMetricsBucketIdentifier, FrmMetricRow)>> {
        match self {
            Self::FrmTriggeredAttempts => {
                FrmTriggeredAttempts::default()
                    .load_metrics(
                        dimensions,
                        merchant_id,
                        filters,
                        granularity,
                        time_range,
                        pool,
                    )
                    .await
            }
            Self::FrmBlockedRate => {
                FrmBlockedRate::default()
                    .load_metrics(
                        dimensions,
                        merchant_id,
                        filters,
                        granularity,
                        time_range,
                        pool,
                    )
                    .await
            }
        }
    }
}


// File: crates/analytics/src/frm/filters.rs

use api_models::analytics::{
    frm::{FrmDimensions, FrmTransactionType},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use diesel_models::enums::FraudCheckStatus;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window},
    types::{
        AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult,
        LoadRow,
    },
};
pub trait FrmFilterAnalytics: LoadRow<FrmFilterRow> {}

pub async fn get_frm_filter_for_dimension<T>(
    dimension: FrmDimensions,
    merchant_id: &common_utils::id_type::MerchantId,
    time_range: &TimeRange,
    pool: &T,
) -> FiltersResult<Vec<FrmFilterRow>>
where
    T: AnalyticsDataSource + FrmFilterAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::FraudCheck);

    query_builder.add_select_column(dimension).switch()?;
    time_range
        .set_filter_clause(&mut query_builder)
        .attach_printable("Error filtering time range")
        .switch()?;

    query_builder
        .add_filter_clause("merchant_id", merchant_id)
        .switch()?;

    query_builder.set_distinct();

    query_builder
        .execute_query::<FrmFilterRow, _>(pool)
        .await
        .change_context(FiltersError::QueryBuildingError)?
        .change_context(FiltersError::QueryExecutionFailure)
}

#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)]
pub struct FrmFilterRow {
    pub frm_status: Option<DBEnumWrapper<FraudCheckStatus>>,
    pub frm_transaction_type: Option<DBEnumWrapper<FrmTransactionType>>,
    pub frm_name: Option<String>,
}


// File: crates/analytics/src/frm/accumulator.rs

use api_models::analytics::frm::FrmMetricsBucketValue;
use common_enums::enums as storage_enums;

use super::metrics::FrmMetricRow;
#[derive(Debug, Default)]
pub struct FrmMetricsAccumulator {
    pub frm_triggered_attempts: TriggeredAttemptsAccumulator,
    pub frm_blocked_rate: BlockedRateAccumulator,
}

#[derive(Debug, Default)]
#[repr(transparent)]
pub struct TriggeredAttemptsAccumulator {
    pub count: Option<i64>,
}

#[derive(Debug, Default)]
pub struct BlockedRateAccumulator {
    pub fraud: i64,
    pub total: i64,
}

pub trait FrmMetricAccumulator {
    type MetricOutput;

    fn add_metrics_bucket(&mut self, metrics: &FrmMetricRow);

    fn collect(self) -> Self::MetricOutput;
}

impl FrmMetricAccumulator for TriggeredAttemptsAccumulator {
    type MetricOutput = Option<u64>;
    #[inline]
    fn add_metrics_bucket(&mut self, metrics: &FrmMetricRow) {
        self.count = match (self.count, metrics.count) {
            (None, None) => None,
            (None, i @ Some(_)) | (i @ Some(_), None) => i,
            (Some(a), Some(b)) => Some(a + b),
        }
    }
    #[inline]
    fn collect(self) -> Self::MetricOutput {
        self.count.and_then(|i| u64::try_from(i).ok())
    }
}

impl FrmMetricAccumulator for BlockedRateAccumulator {
    type MetricOutput = Option<f64>;

    fn add_metrics_bucket(&mut self, metrics: &FrmMetricRow) {
        if let Some(ref frm_status) = metrics.frm_status {
            if frm_status.as_ref() == &storage_enums::FraudCheckStatus::Fraud {
                self.fraud += metrics.count.unwrap_or_default();
            }
        };
        self.total += metrics.count.unwrap_or_default();
    }

    fn collect(self) -> Self::MetricOutput {
        if self.total <= 0 {
            None
        } else {
            Some(
                f64::from(u32::try_from(self.fraud).ok()?) * 100.0
                    / f64::from(u32::try_from(self.total).ok()?),
            )
        }
    }
}

impl FrmMetricsAccumulator {
    pub fn collect(self) -> FrmMetricsBucketValue {
        FrmMetricsBucketValue {
            frm_blocked_rate: self.frm_blocked_rate.collect(),
            frm_triggered_attempts: self.frm_triggered_attempts.collect(),
        }
    }
}


// File: crates/analytics/src/frm/metrics/frm_triggered_attempts.rs

use api_models::analytics::{
    frm::{FrmDimensions, FrmFilters, FrmMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::FrmMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
};

#[derive(Default)]
pub(super) struct FrmTriggeredAttempts {}

#[async_trait::async_trait]
impl<T> super::FrmMetric<T> for FrmTriggeredAttempts
where
    T: AnalyticsDataSource + super::FrmMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[FrmDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &FrmFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<Vec<(FrmMetricsBucketIdentifier, FrmMetricRow)>> {
        let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::FraudCheck);

        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }

        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        filters.set_filter_clause(&mut query_builder).switch()?;

        query_builder
            .add_filter_clause("merchant_id", merchant_id)
            .switch()?;

        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<FrmMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    FrmMetricsBucketIdentifier::new(
                        i.frm_name.as_ref().map(|i| i.to_string()),
                        i.frm_status.as_ref().map(|i| i.0.to_string()),
                        i.frm_transaction_type.as_ref().map(|i| i.0.to_string()),
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<Vec<_>, crate::query::PostProcessingError>>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/frm/metrics/frm_blocked_rate.rs

use api_models::analytics::{
    frm::{FrmDimensions, FrmFilters, FrmMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::FrmMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
};
#[derive(Default)]
pub(super) struct FrmBlockedRate {}

#[async_trait::async_trait]
impl<T> super::FrmMetric<T> for FrmBlockedRate
where
    T: AnalyticsDataSource + super::FrmMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        dimensions: &[FrmDimensions],
        merchant_id: &common_utils::id_type::MerchantId,
        filters: &FrmFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<Vec<(FrmMetricsBucketIdentifier, FrmMetricRow)>>
    where
        T: AnalyticsDataSource + super::FrmMetricAnalytics,
    {
        let mut query_builder = QueryBuilder::new(AnalyticsCollection::FraudCheck);
        let mut dimensions = dimensions.to_vec();

        dimensions.push(FrmDimensions::FrmStatus);

        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }

        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;
        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        filters.set_filter_clause(&mut query_builder).switch()?;

        query_builder
            .add_filter_clause("merchant_id", merchant_id)
            .switch()?;

        time_range.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder.add_group_by_clause(dim).switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .switch()?;
        }

        query_builder
            .execute_query::<FrmMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    FrmMetricsBucketIdentifier::new(
                        i.frm_name.as_ref().map(|i| i.to_string()),
                        None,
                        i.frm_transaction_type.as_ref().map(|i| i.0.to_string()),
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                Vec<(FrmMetricsBucketIdentifier, FrmMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/core.rs

use std::collections::HashMap;

use api_models::analytics::{
    auth_events::{
        AuthEventDimensions, AuthEventMetrics, AuthEventMetricsBucketIdentifier,
        MetricsBucketResponse,
    },
    AuthEventFilterValue, AuthEventFiltersResponse, AuthEventMetricsResponse,
    AuthEventsAnalyticsMetadata, GetAuthEventFilterRequest, GetAuthEventMetricRequest,
};
use common_utils::types::TimeRange;
use error_stack::{report, ResultExt};
use router_env::{instrument, tracing};

use super::{
    filters::{get_auth_events_filter_for_dimension, AuthEventFilterRow},
    sankey::{get_sankey_data, SankeyRow},
    AuthEventMetricsAccumulator,
};
use crate::{
    auth_events::AuthEventMetricAccumulator,
    enums::AuthInfo,
    errors::{AnalyticsError, AnalyticsResult},
    AnalyticsProvider,
};

#[instrument(skip_all)]
pub async fn get_metrics(
    pool: &AnalyticsProvider,
    auth: &AuthInfo,
    req: GetAuthEventMetricRequest,
) -> AnalyticsResult<AuthEventMetricsResponse<MetricsBucketResponse>> {
    let mut metrics_accumulator: HashMap<
        AuthEventMetricsBucketIdentifier,
        AuthEventMetricsAccumulator,
    > = HashMap::new();

    let mut set = tokio::task::JoinSet::new();
    for metric_type in req.metrics.iter().cloned() {
        let req = req.clone();
        let auth_scoped = auth.to_owned();
        let pool = pool.clone();
        set.spawn(async move {
            let data = pool
                .get_auth_event_metrics(
                    &metric_type,
                    &req.group_by_names.clone(),
                    &auth_scoped,
                    &req.filters,
                    req.time_series.map(|t| t.granularity),
                    &req.time_range,
                )
                .await
                .change_context(AnalyticsError::UnknownError);
            (metric_type, data)
        });
    }

    while let Some((metric, data)) = set
        .join_next()
        .await
        .transpose()
        .change_context(AnalyticsError::UnknownError)?
    {
        for (id, value) in data? {
            let metrics_builder = metrics_accumulator.entry(id).or_default();
            match metric {
                AuthEventMetrics::AuthenticationCount => metrics_builder
                    .authentication_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationAttemptCount => metrics_builder
                    .authentication_attempt_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationSuccessCount => metrics_builder
                    .authentication_success_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::ChallengeFlowCount => metrics_builder
                    .challenge_flow_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::ChallengeAttemptCount => metrics_builder
                    .challenge_attempt_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::ChallengeSuccessCount => metrics_builder
                    .challenge_success_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::FrictionlessFlowCount => metrics_builder
                    .frictionless_flow_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::FrictionlessSuccessCount => metrics_builder
                    .frictionless_success_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationErrorMessage => metrics_builder
                    .authentication_error_message
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationFunnel => metrics_builder
                    .authentication_funnel
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationExemptionApprovedCount => metrics_builder
                    .authentication_exemption_approved_count
                    .add_metrics_bucket(&value),
                AuthEventMetrics::AuthenticationExemptionRequestedCount => metrics_builder
                    .authentication_exemption_requested_count
                    .add_metrics_bucket(&value),
            }
        }
    }

    let mut total_error_message_count = 0;
    let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
        .into_iter()
        .map(|(id, val)| {
            let collected_values = val.collect();
            if let Some(count) = collected_values.error_message_count {
                total_error_message_count += count;
            }
            MetricsBucketResponse {
                values: collected_values,
                dimensions: id,
            }
        })
        .collect();
    Ok(AuthEventMetricsResponse {
        query_data,
        meta_data: [AuthEventsAnalyticsMetadata {
            total_error_message_count: Some(total_error_message_count),
        }],
    })
}

pub async fn get_filters(
    pool: &AnalyticsProvider,
    req: GetAuthEventFilterRequest,
    auth: &AuthInfo,
) -> AnalyticsResult<AuthEventFiltersResponse> {
    let mut res = AuthEventFiltersResponse::default();
    for dim in req.group_by_names {
        let values = match pool {
                        AnalyticsProvider::Sqlx(_pool) => {
                            Err(report!(AnalyticsError::UnknownError))
            }
                        AnalyticsProvider::Clickhouse(pool) => {
                get_auth_events_filter_for_dimension(dim, auth, &req.time_range, pool)
                    .await
                    .map_err(|e| e.change_context(AnalyticsError::UnknownError))
            }
                    AnalyticsProvider::CombinedCkh(sqlx_pool, ckh_pool) | AnalyticsProvider::CombinedSqlx(sqlx_pool, ckh_pool) => {
                let ckh_result = get_auth_events_filter_for_dimension(
                    dim,
                    auth,
                    &req.time_range,
                    ckh_pool,
                )
                .await
                .map_err(|e| e.change_context(AnalyticsError::UnknownError));
                let sqlx_result = get_auth_events_filter_for_dimension(
                    dim,
                    auth,
                    &req.time_range,
                    sqlx_pool,
                )
                .await
                .map_err(|e| e.change_context(AnalyticsError::UnknownError));
                match (&sqlx_result, &ckh_result) {
                    (Ok(ref sqlx_res), Ok(ref ckh_res)) if sqlx_res != ckh_res => {
                        router_env::logger::error!(clickhouse_result=?ckh_res, postgres_result=?sqlx_res, "Mismatch between clickhouse & postgres refunds analytics filters")
                    },
                    _ => {}
                };
                ckh_result
            }
        }
        .change_context(AnalyticsError::UnknownError)?
        .into_iter()
        .filter_map(|fil: AuthEventFilterRow| match dim {
            AuthEventDimensions::AuthenticationStatus => fil.authentication_status.map(|i| i.as_ref().to_string()),
            AuthEventDimensions::TransactionStatus => fil.trans_status.map(|i| i.as_ref().to_string()),
            AuthEventDimensions::AuthenticationType => fil.authentication_type.map(|i| i.as_ref().to_string()),
            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,
            AuthEventDimensions::Platform => fil.platform,
            AuthEventDimensions::Mcc => fil.mcc,
           AuthEventDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()),
            AuthEventDimensions::MerchantCountry => fil.merchant_country,
            AuthEventDimensions::BillingCountry => fil.billing_country,
            AuthEventDimensions::ShippingCountry => fil.shipping_country,
            AuthEventDimensions::IssuerCountry => fil.issuer_country,
            AuthEventDimensions::EarliestSupportedVersion => fil.earliest_supported_version,
            AuthEventDimensions::LatestSupportedVersion => fil.latest_supported_version,
            AuthEventDimensions::WhitelistDecision => fil.whitelist_decision.map(|i| i.to_string()),
            AuthEventDimensions::DeviceManufacturer => fil.device_manufacturer,
            AuthEventDimensions::DeviceType => fil.device_type,
            AuthEventDimensions::DeviceBrand => fil.device_brand,
            AuthEventDimensions::DeviceOs => fil.device_os,
            AuthEventDimensions::DeviceDisplay => fil.device_display,
            AuthEventDimensions::BrowserName => fil.browser_name,
            AuthEventDimensions::BrowserVersion => fil.browser_version,
            AuthEventDimensions::IssuerId => fil.issuer_id,
            AuthEventDimensions::SchemeName => fil.scheme_name,
            AuthEventDimensions::ExemptionRequested => fil.exemption_requested.map(|i| i.to_string()),
            AuthEventDimensions::ExemptionAccepted => fil.exemption_accepted.map(|i| i.to_string()),
        })
        .collect::<Vec<String>>();
        res.query_data.push(AuthEventFilterValue {
            dimension: dim,
            values,
        })
    }
    Ok(res)
}

#[instrument(skip_all)]
pub async fn get_sankey(
    pool: &AnalyticsProvider,
    auth: &AuthInfo,
    req: TimeRange,
) -> AnalyticsResult<Vec<SankeyRow>> {
    match pool {
        AnalyticsProvider::Sqlx(_) => Err(AnalyticsError::NotImplemented(
            "Sankey not implemented for sqlx",
        ))?,
        AnalyticsProvider::Clickhouse(ckh_pool)
        | AnalyticsProvider::CombinedCkh(_, ckh_pool)
        | AnalyticsProvider::CombinedSqlx(_, ckh_pool) => {
            let sankey_rows = get_sankey_data(ckh_pool, auth, &req)
                .await
                .change_context(AnalyticsError::UnknownError)?;
            Ok(sankey_rows)
        }
    }
}


// File: crates/analytics/src/auth_events/types.rs

use api_models::analytics::auth_events::{AuthEventDimensions, AuthEventFilters};
use error_stack::ResultExt;

use crate::{
    query::{QueryBuilder, QueryFilter, QueryResult, ToSql},
    types::{AnalyticsCollection, AnalyticsDataSource},
};

impl<T> QueryFilter<T> for AuthEventFilters
where
    T: AnalyticsDataSource,
    AnalyticsCollection: ToSql<T>,
{
    fn set_filter_clause(&self, builder: &mut QueryBuilder<T>) -> QueryResult<()> {
        if !self.authentication_status.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::AuthenticationStatus,
                    &self.authentication_status,
                )
                .attach_printable("Error adding authentication status filter")?;
        }

        if !self.trans_status.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::TransactionStatus,
                    &self.trans_status,
                )
                .attach_printable("Error adding transaction status filter")?;
        }

        if !self.error_message.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::ErrorMessage, &self.error_message)
                .attach_printable("Error adding error message filter")?;
        }

        if !self.authentication_connector.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::AuthenticationConnector,
                    &self.authentication_connector,
                )
                .attach_printable("Error adding authentication connector filter")?;
        }

        if !self.message_version.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::MessageVersion,
                    &self.message_version,
                )
                .attach_printable("Error adding message version filter")?;
        }

        if !self.platform.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::Platform, &self.platform)
                .attach_printable("Error adding platform 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")?;
        }

        if !self.mcc.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::Mcc, &self.mcc)
                .attach_printable("Failed to add MCC filter")?;
        }
        if !self.currency.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::Currency, &self.currency)
                .attach_printable("Failed to add currency filter")?;
        }
        if !self.merchant_country.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::MerchantCountry,
                    &self.merchant_country,
                )
                .attach_printable("Failed to add merchant country filter")?;
        }
        if !self.billing_country.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::BillingCountry,
                    &self.billing_country,
                )
                .attach_printable("Failed to add billing country filter")?;
        }
        if !self.shipping_country.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::ShippingCountry,
                    &self.shipping_country,
                )
                .attach_printable("Failed to add shipping country filter")?;
        }
        if !self.issuer_country.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::IssuerCountry,
                    &self.issuer_country,
                )
                .attach_printable("Failed to add issuer country filter")?;
        }
        if !self.earliest_supported_version.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::EarliestSupportedVersion,
                    &self.earliest_supported_version,
                )
                .attach_printable("Failed to add earliest supported version filter")?;
        }
        if !self.latest_supported_version.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::LatestSupportedVersion,
                    &self.latest_supported_version,
                )
                .attach_printable("Failed to add latest supported version filter")?;
        }
        if !self.whitelist_decision.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::WhitelistDecision,
                    &self.whitelist_decision,
                )
                .attach_printable("Failed to add whitelist decision filter")?;
        }
        if !self.device_manufacturer.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::DeviceManufacturer,
                    &self.device_manufacturer,
                )
                .attach_printable("Failed to add device manufacturer filter")?;
        }
        if !self.device_type.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::DeviceType, &self.device_type)
                .attach_printable("Failed to add device type filter")?;
        }
        if !self.device_brand.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::DeviceBrand, &self.device_brand)
                .attach_printable("Failed to add device brand filter")?;
        }
        if !self.device_os.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::DeviceOs, &self.device_os)
                .attach_printable("Failed to add device OS filter")?;
        }
        if !self.device_display.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::DeviceDisplay,
                    &self.device_display,
                )
                .attach_printable("Failed to add device display filter")?;
        }
        if !self.browser_name.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::BrowserName, &self.browser_name)
                .attach_printable("Failed to add browser name filter")?;
        }
        if !self.browser_version.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::BrowserVersion,
                    &self.browser_version,
                )
                .attach_printable("Failed to add browser version filter")?;
        }
        if !self.issuer_id.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::IssuerId, &self.issuer_id)
                .attach_printable("Failed to add issuer ID filter")?;
        }
        if !self.scheme_name.is_empty() {
            builder
                .add_filter_in_range_clause(AuthEventDimensions::SchemeName, &self.scheme_name)
                .attach_printable("Failed to add scheme name filter")?;
        }
        if !self.exemption_requested.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::ExemptionRequested,
                    &self.exemption_requested,
                )
                .attach_printable("Failed to add exemption requested filter")?;
        }
        if !self.exemption_accepted.is_empty() {
            builder
                .add_filter_in_range_clause(
                    AuthEventDimensions::ExemptionAccepted,
                    &self.exemption_accepted,
                )
                .attach_printable("Failed to add exemption accepted filter")?;
        }

        Ok(())
    }
}


// File: crates/analytics/src/auth_events/metrics.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{
        AuthEventDimensions, AuthEventFilters, AuthEventMetrics, AuthEventMetricsBucketIdentifier,
    },
    Granularity, TimeRange,
};
use diesel_models::enums as storage_enums;
use time::PrimitiveDateTime;

use crate::{
    query::{Aggregate, GroupByClause, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult},
    AuthInfo,
};

mod authentication_attempt_count;
mod authentication_count;
mod authentication_error_message;
mod authentication_exemption_approved_count;
mod authentication_exemption_requested_count;
mod authentication_funnel;
mod authentication_success_count;
mod challenge_attempt_count;
mod challenge_flow_count;
mod challenge_success_count;
mod frictionless_flow_count;
mod frictionless_success_count;

use authentication_attempt_count::AuthenticationAttemptCount;
use authentication_count::AuthenticationCount;
use authentication_error_message::AuthenticationErrorMessage;
use authentication_exemption_approved_count::AuthenticationExemptionApprovedCount;
use authentication_exemption_requested_count::AuthenticationExemptionRequestedCount;
use authentication_funnel::AuthenticationFunnel;
use authentication_success_count::AuthenticationSuccessCount;
use challenge_attempt_count::ChallengeAttemptCount;
use challenge_flow_count::ChallengeFlowCount;
use challenge_success_count::ChallengeSuccessCount;
use frictionless_flow_count::FrictionlessFlowCount;
use frictionless_success_count::FrictionlessSuccessCount;

#[derive(Debug, PartialEq, Eq, serde::Deserialize, Hash)]
pub struct AuthEventMetricRow {
    pub count: Option<i64>,
    pub authentication_status: Option<DBEnumWrapper<storage_enums::AuthenticationStatus>>,
    pub trans_status: Option<DBEnumWrapper<storage_enums::TransactionStatus>>,
    pub authentication_type: Option<DBEnumWrapper<storage_enums::DecoupledAuthenticationType>>,
    pub error_message: Option<String>,
    pub authentication_connector: Option<DBEnumWrapper<storage_enums::AuthenticationConnectors>>,
    pub message_version: Option<String>,
    pub acs_reference_number: Option<String>,
    pub platform: Option<String>,
    pub mcc: Option<String>,
    pub currency: Option<DBEnumWrapper<storage_enums::Currency>>,
    pub merchant_country: Option<String>,
    pub billing_country: Option<String>,
    pub shipping_country: Option<String>,
    pub issuer_country: Option<String>,
    pub earliest_supported_version: Option<String>,
    pub latest_supported_version: Option<String>,
    pub whitelist_decision: Option<bool>,
    pub device_manufacturer: Option<String>,
    pub device_type: Option<String>,
    pub device_brand: Option<String>,
    pub device_os: Option<String>,
    pub device_display: Option<String>,
    pub browser_name: Option<String>,
    pub browser_version: Option<String>,
    pub issuer_id: Option<String>,
    pub scheme_name: Option<String>,
    pub exemption_requested: Option<bool>,
    pub exemption_accepted: Option<bool>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub start_bucket: Option<PrimitiveDateTime>,
    #[serde(with = "common_utils::custom_serde::iso8601::option")]
    pub end_bucket: Option<PrimitiveDateTime>,
}

pub trait AuthEventMetricAnalytics: LoadRow<AuthEventMetricRow> {}

#[async_trait::async_trait]
pub trait AuthEventMetric<T>
where
    T: AnalyticsDataSource + AuthEventMetricAnalytics,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>>;
}

#[async_trait::async_trait]
impl<T> AuthEventMetric<T> for AuthEventMetrics
where
    T: AnalyticsDataSource + AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        match self {
            Self::AuthenticationCount => {
                AuthenticationCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationAttemptCount => {
                AuthenticationAttemptCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationSuccessCount => {
                AuthenticationSuccessCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::ChallengeFlowCount => {
                ChallengeFlowCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::ChallengeAttemptCount => {
                ChallengeAttemptCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::ChallengeSuccessCount => {
                ChallengeSuccessCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::FrictionlessFlowCount => {
                FrictionlessFlowCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::FrictionlessSuccessCount => {
                FrictionlessSuccessCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationErrorMessage => {
                AuthenticationErrorMessage
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationFunnel => {
                AuthenticationFunnel
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationExemptionApprovedCount => {
                AuthenticationExemptionApprovedCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
            Self::AuthenticationExemptionRequestedCount => {
                AuthenticationExemptionRequestedCount
                    .load_metrics(auth, dimensions, filters, granularity, time_range, pool)
                    .await
            }
        }
    }
}


// File: crates/analytics/src/auth_events/sankey.rs

use common_enums::AuthenticationStatus;
use common_utils::{
    errors::ParsingError,
    types::{authentication::AuthInfo, TimeRange},
};
use error_stack::ResultExt;
use router_env::logger;

use crate::{
    clickhouse::ClickhouseClient,
    query::{Aggregate, QueryBuilder, QueryFilter},
    types::{AnalyticsCollection, MetricsError, MetricsResult},
};

#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct SankeyRow {
    pub count: i64,
    pub authentication_status: Option<AuthenticationStatus>,
    pub exemption_requested: Option<bool>,
    pub exemption_accepted: Option<bool>,
}

impl TryInto<SankeyRow> for serde_json::Value {
    type Error = error_stack::Report<ParsingError>;

    fn try_into(self) -> Result<SankeyRow, Self::Error> {
        logger::debug!("Parsing SankeyRow from {:?}", self);
        serde_json::from_value(self).change_context(ParsingError::StructParseFailure(
            "Failed to parse Sankey in clickhouse results",
        ))
    }
}

pub async fn get_sankey_data(
    clickhouse_client: &ClickhouseClient,
    auth: &AuthInfo,
    time_range: &TimeRange,
) -> MetricsResult<Vec<SankeyRow>> {
    let mut query_builder =
        QueryBuilder::<ClickhouseClient>::new(AnalyticsCollection::Authentications);

    query_builder
        .add_select_column(Aggregate::<String>::Count {
            field: None,
            alias: Some("count"),
        })
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_select_column("exemption_requested")
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_select_column("exemption_accepted")
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_select_column("authentication_status")
        .change_context(MetricsError::QueryBuildingError)?;

    auth.set_filter_clause(&mut query_builder)
        .change_context(MetricsError::QueryBuildingError)?;

    time_range
        .set_filter_clause(&mut query_builder)
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_group_by_clause("exemption_requested")
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_group_by_clause("exemption_accepted")
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .add_group_by_clause("authentication_status")
        .change_context(MetricsError::QueryBuildingError)?;

    query_builder
        .execute_query::<SankeyRow, _>(clickhouse_client)
        .await
        .change_context(MetricsError::QueryBuildingError)?
        .change_context(MetricsError::QueryExecutionFailure)?
        .into_iter()
        .map(Ok)
        .collect()
}


// File: crates/analytics/src/auth_events/filters.rs

use api_models::analytics::{auth_events::AuthEventDimensions, Granularity, TimeRange};
use common_enums::{Currency, DecoupledAuthenticationType};
use common_utils::errors::ReportSwitchExt;
use diesel_models::enums::{AuthenticationConnectors, AuthenticationStatus, TransactionStatus};
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use crate::{
    enums::AuthInfo,
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, ToSql, Window},
    types::{
        AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, FiltersError, FiltersResult,
        LoadRow,
    },
};

pub trait AuthEventFilterAnalytics: LoadRow<AuthEventFilterRow> {}

pub async fn get_auth_events_filter_for_dimension<T>(
    dimension: AuthEventDimensions,
    auth: &AuthInfo,
    time_range: &TimeRange,
    pool: &T,
) -> FiltersResult<Vec<AuthEventFilterRow>>
where
    T: AnalyticsDataSource + AuthEventFilterAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    let mut query_builder: QueryBuilder<T> =
        QueryBuilder::new(AnalyticsCollection::Authentications);

    query_builder.add_select_column(dimension).switch()?;
    time_range
        .set_filter_clause(&mut query_builder)
        .attach_printable("Error filtering time range")
        .switch()?;

    query_builder.set_distinct();

    auth.set_filter_clause(&mut query_builder).switch()?;

    query_builder
        .execute_query::<AuthEventFilterRow, _>(pool)
        .await
        .change_context(FiltersError::QueryBuildingError)?
        .change_context(FiltersError::QueryExecutionFailure)
}

#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)]
pub struct AuthEventFilterRow {
    pub authentication_status: Option<DBEnumWrapper<AuthenticationStatus>>,
    pub trans_status: Option<DBEnumWrapper<TransactionStatus>>,
    pub authentication_type: Option<DBEnumWrapper<DecoupledAuthenticationType>>,
    pub error_message: Option<String>,
    pub authentication_connector: Option<DBEnumWrapper<AuthenticationConnectors>>,
    pub message_version: Option<String>,
    pub acs_reference_number: Option<String>,
    pub platform: Option<String>,
    pub mcc: Option<String>,
    pub currency: Option<DBEnumWrapper<Currency>>,
    pub merchant_country: Option<String>,
    pub billing_country: Option<String>,
    pub shipping_country: Option<String>,
    pub issuer_country: Option<String>,
    pub earliest_supported_version: Option<String>,
    pub latest_supported_version: Option<String>,
    pub whitelist_decision: Option<bool>,
    pub device_manufacturer: Option<String>,
    pub device_type: Option<String>,
    pub device_brand: Option<String>,
    pub device_os: Option<String>,
    pub device_display: Option<String>,
    pub browser_name: Option<String>,
    pub browser_version: Option<String>,
    pub issuer_id: Option<String>,
    pub scheme_name: Option<String>,
    pub exemption_requested: Option<bool>,
    pub exemption_accepted: Option<bool>,
}


// File: crates/analytics/src/auth_events/accumulator.rs

use api_models::analytics::auth_events::AuthEventMetricsBucketValue;

use super::metrics::AuthEventMetricRow;

#[derive(Debug, Default)]
pub struct AuthEventMetricsAccumulator {
    pub authentication_count: CountAccumulator,
    pub authentication_attempt_count: CountAccumulator,
    pub authentication_error_message: AuthenticationErrorMessageAccumulator,
    pub authentication_success_count: CountAccumulator,
    pub challenge_flow_count: CountAccumulator,
    pub challenge_attempt_count: CountAccumulator,
    pub challenge_success_count: CountAccumulator,
    pub frictionless_flow_count: CountAccumulator,
    pub frictionless_success_count: CountAccumulator,
    pub authentication_funnel: CountAccumulator,
    pub authentication_exemption_approved_count: CountAccumulator,
    pub authentication_exemption_requested_count: CountAccumulator,
}

#[derive(Debug, Default)]
#[repr(transparent)]
pub struct CountAccumulator {
    pub count: Option<i64>,
}

#[derive(Debug, Default)]
pub struct AuthenticationErrorMessageAccumulator {
    pub count: Option<i64>,
}

pub trait AuthEventMetricAccumulator {
    type MetricOutput;

    fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow);

    fn collect(self) -> Self::MetricOutput;
}

impl AuthEventMetricAccumulator for CountAccumulator {
    type MetricOutput = Option<u64>;
    #[inline]
    fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow) {
        self.count = match (self.count, metrics.count) {
            (None, None) => None,
            (None, i @ Some(_)) | (i @ Some(_), None) => i,
            (Some(a), Some(b)) => Some(a + b),
        }
    }
    #[inline]
    fn collect(self) -> Self::MetricOutput {
        self.count.and_then(|i| u64::try_from(i).ok())
    }
}

impl AuthEventMetricAccumulator for AuthenticationErrorMessageAccumulator {
    type MetricOutput = Option<u64>;
    #[inline]
    fn add_metrics_bucket(&mut self, metrics: &AuthEventMetricRow) {
        self.count = match (self.count, metrics.count) {
            (None, None) => None,
            (None, i @ Some(_)) | (i @ Some(_), None) => i,
            (Some(a), Some(b)) => Some(a + b),
        }
    }
    #[inline]
    fn collect(self) -> Self::MetricOutput {
        self.count.and_then(|i| u64::try_from(i).ok())
    }
}

impl AuthEventMetricsAccumulator {
    pub fn collect(self) -> AuthEventMetricsBucketValue {
        AuthEventMetricsBucketValue {
            authentication_count: self.authentication_count.collect(),
            authentication_attempt_count: self.authentication_attempt_count.collect(),
            authentication_success_count: self.authentication_success_count.collect(),
            challenge_flow_count: self.challenge_flow_count.collect(),
            challenge_attempt_count: self.challenge_attempt_count.collect(),
            challenge_success_count: self.challenge_success_count.collect(),
            frictionless_flow_count: self.frictionless_flow_count.collect(),
            frictionless_success_count: self.frictionless_success_count.collect(),
            error_message_count: self.authentication_error_message.collect(),
            authentication_funnel: self.authentication_funnel.collect(),
            authentication_exemption_approved_count: self
                .authentication_exemption_approved_count
                .collect(),
            authentication_exemption_requested_count: self
                .authentication_exemption_requested_count
                .collect(),
        }
    }
}


// File: crates/analytics/src/auth_events/metrics/authentication_exemption_approved_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct AuthenticationExemptionApprovedCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for AuthenticationExemptionApprovedCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(AuthEventDimensions::ExemptionAccepted, true)
            .switch()?;
        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/frictionless_flow_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_enums::DecoupledAuthenticationType;
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct FrictionlessFlowCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for FrictionlessFlowCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(
                "authentication_type",
                DecoupledAuthenticationType::Frictionless,
            )
            .switch()?;
        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/frictionless_success_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_enums::{AuthenticationStatus, DecoupledAuthenticationType};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct FrictionlessSuccessCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for FrictionlessSuccessCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(
                "authentication_type",
                DecoupledAuthenticationType::Frictionless,
            )
            .switch()?;

        query_builder
            .add_filter_clause("authentication_status", AuthenticationStatus::Success)
            .switch()?;
        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/challenge_flow_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_enums::DecoupledAuthenticationType;
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct ChallengeFlowCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for ChallengeFlowCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(
                "authentication_type",
                DecoupledAuthenticationType::Challenge,
            )
            .switch()?;
        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }
        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/authentication_exemption_requested_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct AuthenticationExemptionRequestedCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for AuthenticationExemptionRequestedCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(AuthEventDimensions::ExemptionRequested, true)
            .switch()?;

        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/authentication_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct AuthenticationCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for AuthenticationCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}


// File: crates/analytics/src/auth_events/metrics/challenge_attempt_count.rs

use std::collections::HashSet;

use api_models::analytics::{
    auth_events::{AuthEventDimensions, AuthEventFilters, AuthEventMetricsBucketIdentifier},
    Granularity, TimeRange,
};
use common_enums::{AuthenticationStatus, DecoupledAuthenticationType};
use common_utils::errors::ReportSwitchExt;
use error_stack::ResultExt;
use time::PrimitiveDateTime;

use super::AuthEventMetricRow;
use crate::{
    query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
    types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
    AuthInfo,
};

#[derive(Default)]
pub(super) struct ChallengeAttemptCount;

#[async_trait::async_trait]
impl<T> super::AuthEventMetric<T> for ChallengeAttemptCount
where
    T: AnalyticsDataSource + super::AuthEventMetricAnalytics,
    PrimitiveDateTime: ToSql<T>,
    AnalyticsCollection: ToSql<T>,
    Granularity: GroupByClause<T>,
    Aggregate<&'static str>: ToSql<T>,
    Window<&'static str>: ToSql<T>,
{
    async fn load_metrics(
        &self,
        auth: &AuthInfo,
        dimensions: &[AuthEventDimensions],
        filters: &AuthEventFilters,
        granularity: Option<Granularity>,
        time_range: &TimeRange,
        pool: &T,
    ) -> MetricsResult<HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>> {
        let mut query_builder: QueryBuilder<T> =
            QueryBuilder::new(AnalyticsCollection::Authentications);
        for dim in dimensions.iter() {
            query_builder.add_select_column(dim).switch()?;
        }
        query_builder
            .add_select_column(Aggregate::Count {
                field: None,
                alias: Some("count"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Min {
                field: "created_at",
                alias: Some("start_bucket"),
            })
            .switch()?;

        query_builder
            .add_select_column(Aggregate::Max {
                field: "created_at",
                alias: Some("end_bucket"),
            })
            .switch()?;

        query_builder
            .add_filter_clause(
                "authentication_type",
                DecoupledAuthenticationType::Challenge,
            )
            .switch()?;

        query_builder
            .add_filter_in_range_clause(
                "authentication_status",
                &[AuthenticationStatus::Success, AuthenticationStatus::Failed],
            )
            .switch()?;
        filters.set_filter_clause(&mut query_builder).switch()?;
        time_range
            .set_filter_clause(&mut query_builder)
            .attach_printable("Error filtering time range")
            .switch()?;
        auth.set_filter_clause(&mut query_builder).switch()?;

        for dim in dimensions.iter() {
            query_builder
                .add_group_by_clause(dim)
                .attach_printable("Error grouping by dimensions")
                .switch()?;
        }

        if let Some(granularity) = granularity {
            granularity
                .set_group_by_clause(&mut query_builder)
                .attach_printable("Error adding granularity")
                .switch()?;
        }

        query_builder
            .execute_query::<AuthEventMetricRow, _>(pool)
            .await
            .change_context(MetricsError::QueryBuildingError)?
            .change_context(MetricsError::QueryExecutionFailure)?
            .into_iter()
            .map(|i| {
                Ok((
                    AuthEventMetricsBucketIdentifier::new(
                        i.authentication_status.as_ref().map(|i| i.0),
                        i.trans_status.as_ref().map(|i| i.0.clone()),
                        i.authentication_type.as_ref().map(|i| i.0),
                        i.error_message.clone(),
                        i.authentication_connector.as_ref().map(|i| i.0),
                        i.message_version.clone(),
                        i.acs_reference_number.clone(),
                        i.mcc.clone(),
                        i.currency.as_ref().map(|i| i.0),
                        i.merchant_country.clone(),
                        i.billing_country.clone(),
                        i.shipping_country.clone(),
                        i.issuer_country.clone(),
                        i.earliest_supported_version.clone(),
                        i.latest_supported_version.clone(),
                        i.whitelist_decision,
                        i.device_manufacturer.clone(),
                        i.device_type.clone(),
                        i.device_brand.clone(),
                        i.device_os.clone(),
                        i.device_display.clone(),
                        i.browser_name.clone(),
                        i.browser_version.clone(),
                        i.issuer_id.clone(),
                        i.scheme_name.clone(),
                        i.exemption_requested,
                        i.exemption_accepted,
                        TimeRange {
                            start_time: match (granularity, i.start_bucket) {
                                (Some(g), Some(st)) => g.clip_to_start(st)?,
                                _ => time_range.start_time,
                            },
                            end_time: granularity.as_ref().map_or_else(
                                || Ok(time_range.end_time),
                                |g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
                            )?,
                        },
                    ),
                    i,
                ))
            })
            .collect::<error_stack::Result<
                HashSet<(AuthEventMetricsBucketIdentifier, AuthEventMetricRow)>,
                crate::query::PostProcessingError,
            >>()
            .change_context(MetricsError::PostProcessingFailure)
    }
}