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 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/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 9c4770c79eee..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(); @@ -86,16 +90,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 => metrics_builder - .refund_success_rate - .add_metrics_bucket(&value), - RefundMetrics::RefundCount => { + 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::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) } } @@ -107,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.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/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/mod.rs b/crates/analytics/src/refunds/metrics/sessionized_metrics/mod.rs new file mode 100644 index 000000000000..bb404cd34101 --- /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}; 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..c77e1f7a52c1 --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_count.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 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..c91938228af1 --- /dev/null +++ b/crates/analytics/src/refunds/metrics/sessionized_metrics/refund_processed_amount.rs @@ -0,0 +1,129 @@ +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("currency").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()?; + } + + query_builder.add_group_by_clause("currency").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) + } +} diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 8d63bc3096ca..70c0e0e78dc8 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, @@ -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/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, diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index ef17387d1ea4..d981bd4382f1 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 { @@ -176,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/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 { 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) },