From d2dbc4a44b5e7a3fa8e2e0e367493726bba19d52 Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 24 Oct 2024 00:47:48 +0530 Subject: [PATCH 1/7] feat(analytics): create module sessionized_metrics for refunds --- .../metrics/sessionized_metrics/mod.rs | 11 ++ .../sessionized_metrics/refund_count.rs | 119 +++++++++++++++++ .../refund_processed_amount.rs | 125 ++++++++++++++++++ .../refund_success_count.rs | 125 ++++++++++++++++++ .../refund_success_rate.rs | 120 +++++++++++++++++ 5 files changed, 500 insertions(+) create mode 100644 crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs create mode 100644 crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs create mode 100644 crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs create mode 100644 crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_count.rs create mode 100644 crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_rate.rs diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs new file mode 100644 index 000000000000..6f6f5ea74c86 --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs @@ -0,0 +1,11 @@ +mod refund_count; +mod refund_processed_amount; +mod refund_success_count; +mod refund_success_rate; + +pub(super) use refund_count::RefundCount; +pub(super) use refund_processed_amount::RefundProcessedAmount; +pub(super) use refund_success_count::RefundSuccessCount; +pub(super) use refund_success_rate::RefundSuccessRate; + +pub use super::{RefundMetric, RefundMetricAnalytics, RefundMetricRow}; \ No newline at end of file diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs new file mode 100644 index 000000000000..d326f3bd68fe --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs @@ -0,0 +1,119 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(crate) struct RefundCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + auth: &AuthInfo, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + + 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()?; + + auth.set_filter_clause(&mut query_builder).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.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + i.refund_status.as_ref().map(|i| i.0.to_string()), + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + i.profile_id.clone(), + 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::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs new file mode 100644 index 000000000000..bfc3c4fd6d2d --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs @@ -0,0 +1,125 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(crate) struct RefundProcessedAmount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundProcessedAmount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + auth: &AuthInfo, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + + query_builder + .add_select_column(Aggregate::Sum { + field: "refund_amount", + alias: Some("total"), + }) + .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()?; + + auth.set_filter_clause(&mut query_builder).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).switch()?; + } + + if let Some(granularity) = granularity.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + i.profile_id.clone(), + 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::, crate::query::PostProcessingError>>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_count.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_count.rs new file mode 100644 index 000000000000..332261a32083 --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_count.rs @@ -0,0 +1,125 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums as storage_enums; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(crate) struct RefundSuccessCount {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessCount +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + auth: &AuthInfo, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + + 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()?; + + auth.set_filter_clause(&mut query_builder).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.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .add_filter_clause( + RefundDimensions::RefundStatus, + storage_enums::RefundStatus::Success, + ) + .switch()?; + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + i.profile_id.clone(), + 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::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_rate.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_rate.rs new file mode 100644 index 000000000000..35ee0d61b509 --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_success_rate.rs @@ -0,0 +1,120 @@ +use std::collections::HashSet; + +use api_models::analytics::{ + refunds::{RefundDimensions, RefundFilters, RefundMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::RefundMetricRow; +use crate::{ + enums::AuthInfo, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; +#[derive(Default)] +pub(crate) struct RefundSuccessRate {} + +#[async_trait::async_trait] +impl super::RefundMetric for RefundSuccessRate +where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + PrimitiveDateTime: ToSql, + AnalyticsCollection: ToSql, + Granularity: GroupByClause, + Aggregate<&'static str>: ToSql, + Window<&'static str>: ToSql, +{ + async fn load_metrics( + &self, + dimensions: &[RefundDimensions], + auth: &AuthInfo, + filters: &RefundFilters, + granularity: &Option, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult> + where + T: AnalyticsDataSource + super::RefundMetricAnalytics, + { + let mut query_builder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + let mut dimensions = dimensions.to_vec(); + + dimensions.push(RefundDimensions::RefundStatus); + + 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()?; + + auth.set_filter_clause(&mut query_builder).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.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .switch()?; + } + + query_builder + .execute_query::(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + RefundMetricsBucketIdentifier::new( + i.currency.as_ref().map(|i| i.0), + None, + i.connector.clone(), + i.refund_type.as_ref().map(|i| i.0.to_string()), + i.profile_id.clone(), + 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::, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} From 06b27b6e21c655d4a1d48088dfcb61ee825a9b88 Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 24 Oct 2024 00:50:00 +0530 Subject: [PATCH 2/7] feat(analytics): implement sessionized_metrics for refunds - extend api_models/src/analytics/refunds.rs - extend metrics to include these enum variants --- crates/analytics/src/refunds/core.rs | 8 ++++---- crates/analytics/src/refunds/metrics.rs | 21 +++++++++++++++++++++ crates/api_models/src/analytics/refunds.rs | 4 ++++ 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs index 9c4770c79eee..1ae0540c084e 100644 --- a/crates/analytics/src/refunds/core.rs +++ b/crates/analytics/src/refunds/core.rs @@ -86,16 +86,16 @@ pub async fn get_metrics( logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); let metrics_builder = metrics_accumulator.entry(id).or_default(); match metric { - RefundMetrics::RefundSuccessRate => metrics_builder + RefundMetrics::RefundSuccessRate | RefundMetrics::SessionizedRefundSuccessRate => metrics_builder .refund_success_rate .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { + RefundMetrics::RefundCount | RefundMetrics::SessionizedRefundCount=> { metrics_builder.refund_count.add_metrics_bucket(&value) } - RefundMetrics::RefundSuccessCount => { + RefundMetrics::RefundSuccessCount | RefundMetrics::SessionizedRefundSuccessCount => { metrics_builder.refund_success.add_metrics_bucket(&value) } - RefundMetrics::RefundProcessedAmount => { + RefundMetrics::RefundProcessedAmount | RefundMetrics::SessionizedRefundProcessedAmount=> { metrics_builder.processed_amount.add_metrics_bucket(&value) } } diff --git a/crates/analytics/src/refunds/metrics.rs b/crates/analytics/src/refunds/metrics.rs index 6ecfd8aeb293..c211ea82d7af 100644 --- a/crates/analytics/src/refunds/metrics.rs +++ b/crates/analytics/src/refunds/metrics.rs @@ -10,6 +10,7 @@ mod refund_count; mod refund_processed_amount; mod refund_success_count; mod refund_success_rate; +mod sessionized_metrics; use std::collections::HashSet; use refund_count::RefundCount; @@ -101,6 +102,26 @@ where .load_metrics(dimensions, auth, filters, granularity, time_range, pool) .await } + Self::SessionizedRefundSuccessRate => { + sessionized_metrics::RefundSuccessRate::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } + Self::SessionizedRefundCount => { + sessionized_metrics::RefundCount::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } + Self::SessionizedRefundSuccessCount => { + sessionized_metrics::RefundSuccessCount::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } + Self::SessionizedRefundProcessedAmount => { + sessionized_metrics::RefundProcessedAmount::default() + .load_metrics(dimensions, auth, filters, granularity, time_range, pool) + .await + } } } } diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index 8acb51e764c0..bed9c01fa4c1 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -88,6 +88,10 @@ pub enum RefundMetrics { RefundCount, RefundSuccessCount, RefundProcessedAmount, + SessionizedRefundSuccessRate, + SessionizedRefundCount, + SessionizedRefundSuccessCount, + SessionizedRefundProcessedAmount, } pub mod metric_behaviour { From 704d4afc2c72c70fe3b5cce90c9d6007bf2f235c Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 20:00:15 +0000 Subject: [PATCH 3/7] chore: run formatter --- crates/analytics/src/refunds/core.rs | 16 ++++++++++------ .../refunds/metrics/sessionized_metrics/mod.rs | 2 +- .../metrics/sessionized_metrics/refund_count.rs | 3 ++- .../refund_processed_amount.rs | 3 ++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs index 1ae0540c084e..6a8185d10bc8 100644 --- a/crates/analytics/src/refunds/core.rs +++ b/crates/analytics/src/refunds/core.rs @@ -86,16 +86,20 @@ pub async fn get_metrics( logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}"); let metrics_builder = metrics_accumulator.entry(id).or_default(); match metric { - RefundMetrics::RefundSuccessRate | RefundMetrics::SessionizedRefundSuccessRate => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount | RefundMetrics::SessionizedRefundCount=> { + RefundMetrics::RefundSuccessRate | RefundMetrics::SessionizedRefundSuccessRate => { + metrics_builder + .refund_success_rate + .add_metrics_bucket(&value) + } + RefundMetrics::RefundCount | RefundMetrics::SessionizedRefundCount => { metrics_builder.refund_count.add_metrics_bucket(&value) } - RefundMetrics::RefundSuccessCount | RefundMetrics::SessionizedRefundSuccessCount => { + RefundMetrics::RefundSuccessCount + | RefundMetrics::SessionizedRefundSuccessCount => { metrics_builder.refund_success.add_metrics_bucket(&value) } - RefundMetrics::RefundProcessedAmount | RefundMetrics::SessionizedRefundProcessedAmount=> { + RefundMetrics::RefundProcessedAmount + | RefundMetrics::SessionizedRefundProcessedAmount => { metrics_builder.processed_amount.add_metrics_bucket(&value) } } diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs index 6f6f5ea74c86..bb404cd34101 100644 --- a/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs @@ -8,4 +8,4 @@ pub(super) use refund_processed_amount::RefundProcessedAmount; pub(super) use refund_success_count::RefundSuccessCount; pub(super) use refund_success_rate::RefundSuccessRate; -pub use super::{RefundMetric, RefundMetricAnalytics, RefundMetricRow}; \ No newline at end of file +pub use super::{RefundMetric, RefundMetricAnalytics, RefundMetricRow}; diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs index d326f3bd68fe..c77e1f7a52c1 100644 --- a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.rs @@ -37,7 +37,8 @@ where time_range: &TimeRange, pool: &T, ) -> MetricsResult> { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::RefundSessionized); for dim in dimensions.iter() { query_builder.add_select_column(dim).switch()?; diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs index bfc3c4fd6d2d..d0aaf025f26e 100644 --- a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs @@ -40,7 +40,8 @@ where where T: AnalyticsDataSource + super::RefundMetricAnalytics, { - let mut query_builder: QueryBuilder = QueryBuilder::new(AnalyticsCollection::RefundSessionized); + let mut query_builder: QueryBuilder = + QueryBuilder::new(AnalyticsCollection::RefundSessionized); for dim in dimensions.iter() { query_builder.add_select_column(dim).switch()?; From 08e53d63b5561163712bdfaefebed1caecdb793b Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 7 Nov 2024 14:33:24 +0530 Subject: [PATCH 4/7] refactor(analytics): rename `*_usd` fields to `*_in_usd` for better consistency --- crates/analytics/src/payments/accumulator.rs | 4 ++-- crates/analytics/src/payments/core.rs | 8 ++++---- crates/api_models/src/analytics.rs | 2 +- crates/api_models/src/analytics/payments.rs | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/analytics/src/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs index 651eeb0bcfe7..5ca9fdf7516a 100644 --- a/crates/analytics/src/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -387,7 +387,7 @@ impl PaymentMetricsAccumulator { payment_processed_count, payment_processed_amount_without_smart_retries, payment_processed_count_without_smart_retries, - payment_processed_amount_usd, + payment_processed_amount_in_usd, payment_processed_amount_without_smart_retries_usd, ) = self.processed_amount.collect(); let ( @@ -417,7 +417,7 @@ impl PaymentMetricsAccumulator { payments_failure_rate_distribution_without_smart_retries, failure_reason_count, failure_reason_count_without_smart_retries, - payment_processed_amount_usd, + payment_processed_amount_in_usd, payment_processed_amount_without_smart_retries_usd, } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs index bcd009270dc1..01a3b1abc15c 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -228,7 +228,7 @@ pub async fn get_metrics( let mut total_payment_processed_count_without_smart_retries = 0; let mut total_failure_reasons_count = 0; let mut total_failure_reasons_count_without_smart_retries = 0; - let mut total_payment_processed_amount_usd = 0; + let mut total_payment_processed_amount_in_usd = 0; let mut total_payment_processed_amount_without_smart_retries_usd = 0; let query_data: Vec = metrics_accumulator .into_iter() @@ -251,9 +251,9 @@ pub async fn get_metrics( }) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .unwrap_or_default(); - collected_values.payment_processed_amount_usd = amount_in_usd; + collected_values.payment_processed_amount_in_usd = amount_in_usd; total_payment_processed_amount += amount; - total_payment_processed_amount_usd += amount_in_usd.unwrap_or(0); + total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); } if let Some(count) = collected_values.payment_processed_count { total_payment_processed_count += count; @@ -299,7 +299,7 @@ pub async fn get_metrics( query_data, meta_data: [PaymentsAnalyticsMetadata { total_payment_processed_amount: Some(total_payment_processed_amount), - total_payment_processed_amount_usd: Some(total_payment_processed_amount_usd), + total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), total_payment_processed_amount_without_smart_retries: Some( total_payment_processed_amount_without_smart_retries, ), diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 8d63bc3096ca..bf7e4956e266 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -203,7 +203,7 @@ pub struct AnalyticsMetadata { #[derive(Debug, serde::Serialize)] pub struct PaymentsAnalyticsMetadata { pub total_payment_processed_amount: Option, - pub total_payment_processed_amount_usd: Option, + pub total_payment_processed_amount_in_usd: Option, pub total_payment_processed_amount_without_smart_retries: Option, pub total_payment_processed_amount_without_smart_retries_usd: Option, pub total_payment_processed_count: Option, diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 1faba79eb378..b34f8c9293cf 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -271,7 +271,7 @@ pub struct PaymentMetricsBucketValue { pub payment_count: Option, pub payment_success_count: Option, pub payment_processed_amount: Option, - pub payment_processed_amount_usd: Option, + pub payment_processed_amount_in_usd: Option, pub payment_processed_count: Option, pub payment_processed_amount_without_smart_retries: Option, pub payment_processed_amount_without_smart_retries_usd: Option, From 6f5fe4eaf1c2b03f78665a19e9436bd6171f656c Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 7 Nov 2024 14:35:11 +0530 Subject: [PATCH 5/7] feat(api_models): add `RefundsAnalyticsMetadata` and `RefundMetricsResponse` structs --- crates/api_models/src/analytics.rs | 11 +++++++++++ crates/api_models/src/events.rs | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index bf7e4956e266..70c0e0e78dc8 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -228,6 +228,11 @@ pub struct PaymentIntentsAnalyticsMetadata { pub total_payment_processed_count_without_smart_retries: Option, } +#[derive(Debug, serde::Serialize)] +pub struct RefundsAnalyticsMetadata { + pub total_refund_processed_amount: Option, + pub total_refund_processed_amount_in_usd: Option, +} #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentFiltersRequest { @@ -362,6 +367,12 @@ pub struct PaymentIntentsMetricsResponse { pub meta_data: [PaymentIntentsAnalyticsMetadata; 1], } +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RefundsMetricsResponse { + pub query_data: Vec, + pub meta_data: [RefundsAnalyticsMetadata; 1], +} #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetApiEventFiltersRequest { diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index 7272abffbff6..77d8cb117ef4 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -168,6 +168,11 @@ impl ApiEventMetric for PaymentIntentsMetricsResponse { } } +impl ApiEventMetric for RefundsMetricsResponse { + fn get_api_event_type(&self) -> Option { + Some(ApiEventsType::Miscellaneous) + } +} #[cfg(all(feature = "v2", feature = "payment_methods_v2"))] impl ApiEventMetric for PaymentMethodIntentConfirmInternal { fn get_api_event_type(&self) -> Option { From 6f56e4a571830a1d515d77e3acd767d32e645b57 Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 7 Nov 2024 14:37:41 +0530 Subject: [PATCH 6/7] feat(analytics): implement currency conversion for refunds analytics --- crates/analytics/src/refunds/accumulator.rs | 15 +++--- crates/analytics/src/refunds/core.rs | 49 +++++++++++++++---- .../metrics/refund_processed_amount.rs | 2 + .../refund_processed_amount.rs | 3 ++ crates/api_models/src/analytics/refunds.rs | 1 + crates/router/src/analytics.rs | 9 ++-- 6 files changed, 61 insertions(+), 18 deletions(-) diff --git a/crates/analytics/src/refunds/accumulator.rs b/crates/analytics/src/refunds/accumulator.rs index 9c51defdcf91..add38c98162c 100644 --- a/crates/analytics/src/refunds/accumulator.rs +++ b/crates/analytics/src/refunds/accumulator.rs @@ -7,7 +7,7 @@ pub struct RefundMetricsAccumulator { pub refund_success_rate: SuccessRateAccumulator, pub refund_count: CountAccumulator, pub refund_success: CountAccumulator, - pub processed_amount: SumAccumulator, + pub processed_amount: PaymentProcessedAmountAccumulator, } #[derive(Debug, Default)] @@ -22,7 +22,7 @@ pub struct CountAccumulator { } #[derive(Debug, Default)] #[repr(transparent)] -pub struct SumAccumulator { +pub struct PaymentProcessedAmountAccumulator { pub total: Option, } @@ -50,8 +50,8 @@ impl RefundMetricAccumulator for CountAccumulator { } } -impl RefundMetricAccumulator for SumAccumulator { - type MetricOutput = Option; +impl RefundMetricAccumulator for PaymentProcessedAmountAccumulator { + type MetricOutput = (Option, Option); #[inline] fn add_metrics_bucket(&mut self, metrics: &RefundMetricRow) { self.total = match ( @@ -68,7 +68,7 @@ impl RefundMetricAccumulator for SumAccumulator { } #[inline] fn collect(self) -> Self::MetricOutput { - self.total.and_then(|i| u64::try_from(i).ok()) + (self.total.and_then(|i| u64::try_from(i).ok()), Some(0)) } } @@ -98,11 +98,14 @@ impl RefundMetricAccumulator for SuccessRateAccumulator { impl RefundMetricsAccumulator { pub fn collect(self) -> RefundMetricsBucketValue { + let (refund_processed_amount, refund_processed_amount_in_usd) = + self.processed_amount.collect(); RefundMetricsBucketValue { refund_success_rate: self.refund_success_rate.collect(), refund_count: self.refund_count.collect(), refund_success_count: self.refund_success.collect(), - refund_processed_amount: self.processed_amount.collect(), + refund_processed_amount, + refund_processed_amount_in_usd, } } } diff --git a/crates/analytics/src/refunds/core.rs b/crates/analytics/src/refunds/core.rs index 6a8185d10bc8..e3bfa4da9d10 100644 --- a/crates/analytics/src/refunds/core.rs +++ b/crates/analytics/src/refunds/core.rs @@ -5,9 +5,12 @@ use api_models::analytics::{ refunds::{ RefundDimensions, RefundMetrics, RefundMetricsBucketIdentifier, RefundMetricsBucketResponse, }, - AnalyticsMetadata, GetRefundFilterRequest, GetRefundMetricRequest, MetricsResponse, - RefundFilterValue, RefundFiltersResponse, + GetRefundFilterRequest, GetRefundMetricRequest, RefundFilterValue, RefundFiltersResponse, + RefundsAnalyticsMetadata, RefundsMetricsResponse, }; +use bigdecimal::ToPrimitive; +use common_enums::Currency; +use currency_conversion::{conversion::convert, types::ExchangeRates}; use error_stack::ResultExt; use router_env::{ logger, @@ -29,9 +32,10 @@ use crate::{ pub async fn get_metrics( pool: &AnalyticsProvider, + ex_rates: &ExchangeRates, auth: &AuthInfo, req: GetRefundMetricRequest, -) -> AnalyticsResult> { +) -> AnalyticsResult> { let mut metrics_accumulator: HashMap = HashMap::new(); let mut set = tokio::task::JoinSet::new(); @@ -111,18 +115,45 @@ pub async fn get_metrics( metrics_accumulator ); } + let mut total_refund_processed_amount = 0; + let mut total_refund_processed_amount_in_usd = 0; let query_data: Vec = metrics_accumulator .into_iter() - .map(|(id, val)| RefundMetricsBucketResponse { - values: val.collect(), - dimensions: id, + .map(|(id, val)| { + let mut collected_values = val.collect(); + if let Some(amount) = collected_values.refund_processed_amount { + let amount_in_usd = id + .currency + .and_then(|currency| { + i64::try_from(amount) + .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) + .ok() + .and_then(|amount_i64| { + convert(ex_rates, currency, Currency::USD, amount_i64) + .inspect_err(|e| { + logger::error!("Currency conversion error: {:?}", e) + }) + .ok() + }) + }) + .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) + .unwrap_or_default(); + collected_values.refund_processed_amount_in_usd = amount_in_usd; + total_refund_processed_amount += amount; + total_refund_processed_amount_in_usd += amount_in_usd.unwrap_or(0); + } + RefundMetricsBucketResponse { + values: collected_values, + dimensions: id, + } }) .collect(); - Ok(MetricsResponse { + Ok(RefundsMetricsResponse { query_data, - meta_data: [AnalyticsMetadata { - current_time_range: req.time_range, + meta_data: [RefundsAnalyticsMetadata { + total_refund_processed_amount: Some(total_refund_processed_amount), + total_refund_processed_amount_in_usd: Some(total_refund_processed_amount_in_usd), }], }) } diff --git a/crates/analytics/src/refunds/metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs index f0f51a21fe0f..6cba5f58fed5 100644 --- a/crates/analytics/src/refunds/metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/refund_processed_amount.rs @@ -52,6 +52,7 @@ where alias: Some("total"), }) .switch()?; + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -78,6 +79,7 @@ where query_builder.add_group_by_clause(dim).switch()?; } + query_builder.add_group_by_clause("currency").switch()?; if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs index d0aaf025f26e..c91938228af1 100644 --- a/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs @@ -53,6 +53,7 @@ where alias: Some("total"), }) .switch()?; + query_builder.add_select_column("currency").switch()?; query_builder .add_select_column(Aggregate::Min { field: "created_at", @@ -79,6 +80,8 @@ where query_builder.add_group_by_clause(dim).switch()?; } + query_builder.add_group_by_clause("currency").switch()?; + if let Some(granularity) = granularity.as_ref() { granularity .set_group_by_clause(&mut query_builder) diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index 1b7cac4d5d36..d981bd4382f1 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -180,6 +180,7 @@ pub struct RefundMetricsBucketValue { pub refund_count: Option, pub refund_success_count: Option, pub refund_processed_amount: Option, + pub refund_processed_amount_in_usd: Option, } #[derive(Debug, serde::Serialize)] pub struct RefundMetricsBucketResponse { diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index aba1c2e2efaa..fc45e3f80e62 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -652,7 +652,8 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - analytics::refunds::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -690,7 +691,8 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - analytics::refunds::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, @@ -735,7 +737,8 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - analytics::refunds::get_metrics(&state.pool, &auth, req) + let ex_rates = get_forex_exchange_rates(state.clone()).await?; + analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) }, From 6ae2fb68290bf91a6d10178aa620893c3fd45846 Mon Sep 17 00:00:00 2001 From: uzair khan Date: Thu, 7 Nov 2024 19:02:00 +0530 Subject: [PATCH 7/7] fix(analytics): sanitize sankey response data with correct logic --- crates/analytics/src/payment_intents/core.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 7ea8e9007f7b..b242b3dae81e 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -69,11 +69,21 @@ pub async fn get_sankey( i.refunds_status.unwrap_or_default().as_ref(), i.attempt_count, ) { + (IntentStatus::Succeeded, SessionizerRefundStatus::FullRefunded, 1) => { + sankey_response.refunded += i.count; + sankey_response.normal_success += i.count + } + (IntentStatus::Succeeded, SessionizerRefundStatus::PartialRefunded, 1) => { + sankey_response.partial_refunded += i.count; + sankey_response.normal_success += i.count + } (IntentStatus::Succeeded, SessionizerRefundStatus::FullRefunded, _) => { - sankey_response.refunded += i.count + sankey_response.refunded += i.count; + sankey_response.smart_retried_success += i.count } (IntentStatus::Succeeded, SessionizerRefundStatus::PartialRefunded, _) => { - sankey_response.partial_refunded += i.count + sankey_response.partial_refunded += i.count; + sankey_response.smart_retried_success += i.count } ( IntentStatus::Succeeded