From ae5da43b8bef301afb4163df5c9dc0f9c13b40cc Mon Sep 17 00:00:00 2001 From: T Sandeep Kumar <sandeep.kumar@juspay.in> Date: Fri, 28 Jun 2024 00:47:00 +0530 Subject: [PATCH 1/5] payment_intent analytics added for 4 metrics --- crates/analytics/src/clickhouse.rs | 23 ++ crates/analytics/src/core.rs | 5 + crates/analytics/src/lib.rs | 109 +++++++++ crates/analytics/src/payment_intents.rs | 15 ++ .../src/payment_intents/accumulator.rs | 89 +++++++ crates/analytics/src/payment_intents/core.rs | 226 ++++++++++++++++++ .../analytics/src/payment_intents/filters.rs | 56 +++++ .../analytics/src/payment_intents/metrics.rs | 124 ++++++++++ .../metrics/payment_intent_count.rs | 118 +++++++++ .../metrics/smart_retried_amount.rs | 117 +++++++++ .../metrics/successful_smart_retries.rs | 127 ++++++++++ .../metrics/total_smart_retries.rs | 125 ++++++++++ crates/analytics/src/payment_intents/types.rs | 28 +++ crates/analytics/src/query.rs | 6 +- crates/analytics/src/sqlx.rs | 62 ++++- crates/analytics/src/types.rs | 1 + crates/analytics/src/utils.rs | 9 + crates/api_models/src/analytics.rs | 37 +++ .../src/analytics/payment_intents.rs | 151 ++++++++++++ crates/api_models/src/events.rs | 18 ++ crates/common_utils/src/events.rs | 1 + crates/router/src/analytics.rs | 75 +++++- 22 files changed, 1518 insertions(+), 4 deletions(-) create mode 100644 crates/analytics/src/payment_intents.rs create mode 100644 crates/analytics/src/payment_intents/accumulator.rs create mode 100644 crates/analytics/src/payment_intents/core.rs create mode 100644 crates/analytics/src/payment_intents/filters.rs create mode 100644 crates/analytics/src/payment_intents/metrics.rs create mode 100644 crates/analytics/src/payment_intents/metrics/payment_intent_count.rs create mode 100644 crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs create mode 100644 crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs create mode 100644 crates/analytics/src/payment_intents/metrics/total_smart_retries.rs create mode 100644 crates/analytics/src/payment_intents/types.rs create mode 100644 crates/api_models/src/analytics/payment_intents.rs diff --git a/crates/analytics/src/clickhouse.rs b/crates/analytics/src/clickhouse.rs index ab47397b8afa..b455e79b2539 100644 --- a/crates/analytics/src/clickhouse.rs +++ b/crates/analytics/src/clickhouse.rs @@ -10,6 +10,7 @@ use super::{ active_payments::metrics::ActivePaymentsMetricRow, auth_events::metrics::AuthEventMetricRow, health_check::HealthCheck, + payment_intents::{filters::PaymentIntentFilterRow, metrics::PaymentIntentMetricRow}, payments::{ distribution::PaymentDistributionRow, filters::FilterRow, metrics::PaymentMetricRow, }, @@ -157,6 +158,8 @@ where impl super::payments::filters::PaymentFilterAnalytics for ClickhouseClient {} impl super::payments::metrics::PaymentMetricAnalytics for ClickhouseClient {} impl super::payments::distribution::PaymentDistributionAnalytics for ClickhouseClient {} +impl super::payment_intents::filters::PaymentIntentFilterAnalytics for ClickhouseClient {} +impl super::payment_intents::metrics::PaymentIntentMetricAnalytics for ClickhouseClient {} impl super::refunds::metrics::RefundMetricAnalytics for ClickhouseClient {} impl super::refunds::filters::RefundFilterAnalytics for ClickhouseClient {} impl super::sdk_events::filters::SdkEventFilterAnalytics for ClickhouseClient {} @@ -247,6 +250,26 @@ impl TryInto<FilterRow> for serde_json::Value { } } +impl TryInto<PaymentIntentMetricRow> for serde_json::Value { + type Error = Report<ParsingError>; + + fn try_into(self) -> Result<PaymentIntentMetricRow, Self::Error> { + serde_json::from_value(self).change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentIntentMetricRow in clickhouse results", + )) + } +} + +impl TryInto<PaymentIntentFilterRow> for serde_json::Value { + type Error = Report<ParsingError>; + + fn try_into(self) -> Result<PaymentIntentFilterRow, Self::Error> { + serde_json::from_value(self).change_context(ParsingError::StructParseFailure( + "Failed to parse PaymentIntentFilterRow in clickhouse results", + )) + } +} + impl TryInto<RefundMetricRow> for serde_json::Value { type Error = Report<ParsingError>; diff --git a/crates/analytics/src/core.rs b/crates/analytics/src/core.rs index f32783497480..2c5945f75b55 100644 --- a/crates/analytics/src/core.rs +++ b/crates/analytics/src/core.rs @@ -11,6 +11,11 @@ pub async fn get_domain_info( download_dimensions: None, dimensions: utils::get_payment_dimensions(), }, + AnalyticsDomain::PaymentIntents => GetInfoResponse { + metrics: utils::get_payment_intent_metrics_info(), + download_dimensions: None, + dimensions: utils::get_payment_intent_dimensions(), + }, AnalyticsDomain::Refunds => GetInfoResponse { metrics: utils::get_refund_metrics_info(), download_dimensions: None, diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index d3db03a6977b..ea70257d6812 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -4,6 +4,7 @@ pub mod disputes; pub mod errors; pub mod metrics; pub mod payments; +pub mod payment_intents; mod query; pub mod refunds; @@ -40,6 +41,7 @@ use api_models::analytics::{ auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier}, disputes::{DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier}, payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, + payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, PaymentIntentMetricsBucketIdentifier}, refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, sdk_events::{ SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, @@ -64,6 +66,7 @@ use self::{ distribution::{PaymentDistribution, PaymentDistributionRow}, metrics::{PaymentMetric, PaymentMetricRow}, }, + payment_intents::metrics::{PaymentIntentMetric, PaymentIntentMetricRow}, refunds::metrics::{RefundMetric, RefundMetricRow}, sdk_events::metrics::{SdkEventMetric, SdkEventMetricRow}, sqlx::SqlxClient, @@ -313,6 +316,110 @@ impl AnalyticsProvider { .await } + pub async fn get_payment_intent_metrics( + &self, + metric: &PaymentIntentMetrics, + dimensions: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + ) -> types::MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + // Metrics to get the fetch time for each payment intent metric + metrics::request::record_operation_time( + async { + match self { + Self::Sqlx(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::Clickhouse(pool) => { + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::CombinedCkh(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + 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 payment intents analytics metrics") + }, + _ => {} + + }; + + ckh_result + } + Self::CombinedSqlx(sqlx_pool, ckh_pool) => { + let (ckh_result, sqlx_result) = tokio::join!(metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + ckh_pool, + ), + metric + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + sqlx_pool, + )); + 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 payment intents analytics metrics") + }, + _ => {} + + }; + + sqlx_result + } + } + }, + &metrics::METRIC_FETCH_TIME, + metric, + self, + ) + .await + } + pub async fn get_refund_metrics( &self, metric: &RefundMetrics, @@ -756,11 +863,13 @@ pub struct ReportConfig { pub enum AnalyticsFlow { GetInfo, GetPaymentMetrics, + GetPaymentIntentMetrics, GetRefundsMetrics, GetSdkMetrics, GetAuthMetrics, GetActivePaymentsMetrics, GetPaymentFilters, + GetPaymentIntentFilters, GetRefundFilters, GetSdkEventFilters, GetApiEvents, diff --git a/crates/analytics/src/payment_intents.rs b/crates/analytics/src/payment_intents.rs new file mode 100644 index 000000000000..6f3066d87d48 --- /dev/null +++ b/crates/analytics/src/payment_intents.rs @@ -0,0 +1,15 @@ +pub mod accumulator; +mod core; +pub mod filters; +pub mod metrics; +pub mod types; +pub use accumulator::{ + PaymentIntentMetricAccumulator, PaymentIntentMetricsAccumulator, +}; + +pub trait PaymentIntentAnalytics: + metrics::PaymentIntentMetricAnalytics + filters::PaymentIntentFilterAnalytics +{ +} + +pub use self::core::{get_filters, get_metrics}; diff --git a/crates/analytics/src/payment_intents/accumulator.rs b/crates/analytics/src/payment_intents/accumulator.rs new file mode 100644 index 000000000000..00b89fb90e7e --- /dev/null +++ b/crates/analytics/src/payment_intents/accumulator.rs @@ -0,0 +1,89 @@ +use api_models::analytics::payment_intents::PaymentIntentMetricsBucketValue; +use bigdecimal::ToPrimitive; +use super::metrics::PaymentIntentMetricRow; + +#[derive(Debug, Default)] +pub struct PaymentIntentMetricsAccumulator { + pub successful_smart_retries: CountAccumulator, + pub total_smart_retries: CountAccumulator, + pub smart_retried_amount: SumAccumulator, + pub payment_intent_count: CountAccumulator, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionRow { + pub count: i64, + pub total: i64, + pub error_message: String, +} + +#[derive(Debug, Default)] +pub struct ErrorDistributionAccumulator { + pub error_vec: Vec<ErrorDistributionRow>, +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct CountAccumulator { + pub count: Option<i64>, +} + +pub trait PaymentIntentMetricAccumulator { + type MetricOutput; + + fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow); + + fn collect(self) -> Self::MetricOutput; +} + +#[derive(Debug, Default)] +#[repr(transparent)] +pub struct SumAccumulator { + pub total: Option<i64>, +} + +impl PaymentIntentMetricAccumulator for CountAccumulator { + type MetricOutput = Option<u64>; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) { + 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 PaymentIntentMetricAccumulator for SumAccumulator { + type MetricOutput = Option<u64>; + #[inline] + fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) { + self.total = match ( + self.total, + metrics.total.as_ref().and_then(ToPrimitive::to_i64), + ) { + (None, None) => None, + (None, i @ Some(_)) | (i @ Some(_), None) => i, + (Some(a), Some(b)) => Some(a + b), + } + } + #[inline] + fn collect(self) -> Self::MetricOutput { + self.total.and_then(|i| u64::try_from(i).ok()) + } +} + +impl PaymentIntentMetricsAccumulator { + pub fn collect(self) -> PaymentIntentMetricsBucketValue { + PaymentIntentMetricsBucketValue { + successful_smart_retries: self.successful_smart_retries.collect(), + total_smart_retries: self.total_smart_retries.collect(), + smart_retried_amount: self.smart_retried_amount.collect(), + payment_intent_count: self.payment_intent_count.collect(), + } + } +} diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs new file mode 100644 index 000000000000..e3932baaf077 --- /dev/null +++ b/crates/analytics/src/payment_intents/core.rs @@ -0,0 +1,226 @@ +#![allow(dead_code)] +use std::collections::HashMap; + +use api_models::analytics::{ + payment_intents::{ + MetricsBucketResponse, PaymentIntentDimensions, PaymentIntentMetrics, + PaymentIntentMetricsBucketIdentifier, + }, + AnalyticsMetadata, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, + MetricsResponse, PaymentIntentFilterValue, PaymentIntentFiltersResponse, +}; +use common_utils::errors::CustomResult; +use error_stack::ResultExt; +use router_env::{ + instrument, logger, + metrics::add_attributes, + tracing::{self, Instrument}, +}; + +use super::{ + filters::{get_payment_intent_filter_for_dimension, PaymentIntentFilterRow}, + metrics::PaymentIntentMetricRow, + PaymentIntentMetricsAccumulator, +}; +use crate::{ + errors::{AnalyticsError, AnalyticsResult}, + metrics, + payment_intents::PaymentIntentMetricAccumulator, + AnalyticsProvider, +}; + +#[derive(Debug)] +pub enum TaskType { + MetricTask( + PaymentIntentMetrics, + CustomResult< + Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>, + AnalyticsError, + >, + ), +} + +#[instrument(skip_all)] +pub async fn get_metrics( + pool: &AnalyticsProvider, + merchant_id: &str, + req: GetPaymentIntentMetricRequest, +) -> AnalyticsResult<MetricsResponse<MetricsBucketResponse>> { + let mut metrics_accumulator: HashMap< + PaymentIntentMetricsBucketIdentifier, + PaymentIntentMetricsAccumulator, + > = 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_payment_intents_metrics_query", + payment_metric = metric_type.as_ref() + ); + + // TODO: lifetime issues with joinset, + // can be optimized away if joinset lifetime requirements are relaxed + let merchant_id_scoped = merchant_id.to_owned(); + set.spawn( + async move { + let data = pool + .get_payment_intent_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); + TaskType::MetricTask(metric_type, data) + } + .instrument(task_span), + ); + } + + while let Some(task_type) = set + .join_next() + .await + .transpose() + .change_context(AnalyticsError::UnknownError)? + { + match task_type { + TaskType::MetricTask(metric, data) => { + let data = data?; + let attributes = &add_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(&metrics::CONTEXT, 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 { + PaymentIntentMetrics::SuccessfulSmartRetries => metrics_builder + .successful_smart_retries + .add_metrics_bucket(&value), + PaymentIntentMetrics::TotalSmartRetries => metrics_builder + .total_smart_retries + .add_metrics_bucket(&value), + PaymentIntentMetrics::SmartRetriedAmount => metrics_builder + .smart_retried_amount + .add_metrics_bucket(&value), + PaymentIntentMetrics::PaymentIntentCount => metrics_builder + .payment_intent_count + .add_metrics_bucket(&value), + } + } + + logger::debug!( + "Analytics Accumulated Results: metric: {}, results: {:#?}", + metric, + metrics_accumulator + ); + } + } + } + + let query_data: Vec<MetricsBucketResponse> = metrics_accumulator + .into_iter() + .map(|(id, val)| MetricsBucketResponse { + 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: GetPaymentIntentFiltersRequest, + merchant_id: &String, +) -> AnalyticsResult<PaymentIntentFiltersResponse> { + let mut res = PaymentIntentFiltersResponse::default(); + + for dim in req.group_by_names { + let values = match pool { + AnalyticsProvider::Sqlx(pool) => { + get_payment_intent_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::Clickhouse(pool) => { + get_payment_intent_filter_for_dimension(dim, merchant_id, &req.time_range, pool) + .await + } + AnalyticsProvider::CombinedCkh(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_intent_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_intent_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + 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 payment intents analytics filters") + }, + _ => {} + }; + ckh_result + } + AnalyticsProvider::CombinedSqlx(sqlx_poll, ckh_pool) => { + let ckh_result = get_payment_intent_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + ckh_pool, + ) + .await; + let sqlx_result = get_payment_intent_filter_for_dimension( + dim, + merchant_id, + &req.time_range, + sqlx_poll, + ) + .await; + 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 payment intents analytics filters") + }, + _ => {} + }; + sqlx_result + } + } + .change_context(AnalyticsError::UnknownError)? + .into_iter() + .filter_map(|fil: PaymentIntentFilterRow| match dim { + PaymentIntentDimensions::PaymentIntentStatus => fil.status.map(|i| i.as_ref().to_string()), + PaymentIntentDimensions::Currency => fil.currency.map(|i| i.as_ref().to_string()), + }) + .collect::<Vec<String>>(); + res.query_data.push(PaymentIntentFilterValue { + dimension: dim, + values, + }) + } + Ok(res) +} diff --git a/crates/analytics/src/payment_intents/filters.rs b/crates/analytics/src/payment_intents/filters.rs new file mode 100644 index 000000000000..bea846214799 --- /dev/null +++ b/crates/analytics/src/payment_intents/filters.rs @@ -0,0 +1,56 @@ +use api_models::analytics::{payment_intents::PaymentIntentDimensions, Granularity, TimeRange}; +use common_utils::errors::ReportSwitchExt; +use diesel_models::enums::{IntentStatus, Currency}; +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 PaymentIntentFilterAnalytics: LoadRow<PaymentIntentFilterRow> {} + +pub async fn get_payment_intent_filter_for_dimension<T>( + dimension: PaymentIntentDimensions, + merchant: &String, + time_range: &TimeRange, + pool: &T, +) -> FiltersResult<Vec<PaymentIntentFilterRow>> +where + T: AnalyticsDataSource + PaymentIntentFilterAnalytics, + 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::PaymentIntent); + + 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) + .switch()?; + + query_builder.set_distinct(); + + query_builder + .execute_query::<PaymentIntentFilterRow, _>(pool) + .await + .change_context(FiltersError::QueryBuildingError)? + .change_context(FiltersError::QueryExecutionFailure) +} + +#[derive(Debug, serde::Serialize, Eq, PartialEq, serde::Deserialize)] +pub struct PaymentIntentFilterRow { + pub status: Option<DBEnumWrapper<IntentStatus>>, + pub currency: Option<DBEnumWrapper<Currency>>, +} diff --git a/crates/analytics/src/payment_intents/metrics.rs b/crates/analytics/src/payment_intents/metrics.rs new file mode 100644 index 000000000000..7e9990166f47 --- /dev/null +++ b/crates/analytics/src/payment_intents/metrics.rs @@ -0,0 +1,124 @@ +use api_models::analytics::{ + payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, PaymentIntentMetricsBucketIdentifier}, + 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}, +}; + +mod successful_smart_retries; +mod total_smart_retries; +mod smart_retried_amount; +mod payment_intent_count; + +use successful_smart_retries::SuccessfulSmartRetries; +use total_smart_retries::TotalSmartRetries; +use smart_retried_amount::SmartRetriedAmount; +use payment_intent_count::PaymentIntentCount; + + +#[derive(Debug, PartialEq, Eq, serde::Deserialize)] +pub struct PaymentIntentMetricRow { + pub status: Option<DBEnumWrapper<storage_enums::IntentStatus>>, + pub currency: Option<DBEnumWrapper<storage_enums::Currency>>, + 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 PaymentIntentMetricAnalytics: LoadRow<PaymentIntentMetricRow> {} + +#[async_trait::async_trait] +pub trait PaymentIntentMetric<T> +where + T: AnalyticsDataSource + PaymentIntentMetricAnalytics, +{ + async fn load_metrics( + &self, + dimensions: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>>; +} + +#[async_trait::async_trait] +impl<T> PaymentIntentMetric<T> for PaymentIntentMetrics +where + T: AnalyticsDataSource + PaymentIntentMetricAnalytics, + 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: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + match self { + Self::SuccessfulSmartRetries => { + SuccessfulSmartRetries + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::TotalSmartRetries => { + TotalSmartRetries + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::SmartRetriedAmount => { + SmartRetriedAmount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + Self::PaymentIntentCount => { + PaymentIntentCount + .load_metrics( + dimensions, + merchant_id, + filters, + granularity, + time_range, + pool, + ) + .await + } + } + } +} diff --git a/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs new file mode 100644 index 000000000000..5a1bc5edd8c4 --- /dev/null +++ b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs @@ -0,0 +1,118 @@ +use api_models::analytics::{ + payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentIntentMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct PaymentIntentCount; + +#[async_trait::async_trait] +impl<T> super::PaymentIntentMetric<T> for PaymentIntentCount +where + T: AnalyticsDataSource + super::PaymentIntentMetricAnalytics, + 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: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::PaymentIntent); + + 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.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::<PaymentIntentMetricRow, _>(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentIntentMetricsBucketIdentifier::new( + i.status.as_ref().map(|i| i.0), + i.currency.as_ref().map(|i| i.0), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => 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<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs new file mode 100644 index 000000000000..a25d29fd8bf2 --- /dev/null +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -0,0 +1,117 @@ +use api_models::analytics::{ + payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier}, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentIntentMetricRow; +use crate::{ + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SmartRetriedAmount; + +#[async_trait::async_trait] +impl<T> super::PaymentIntentMetric<T> for SmartRetriedAmount +where + T: AnalyticsDataSource + super::PaymentIntentMetricAnalytics, + 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: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::PaymentIntent); + + for dim in dimensions.iter() { + query_builder.add_select_column(dim).switch()?; + } + query_builder + .add_select_column(Aggregate::Sum { + field: "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()?; + + 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.as_ref() { + granularity + .set_group_by_clause(&mut query_builder) + .attach_printable("Error adding granularity") + .switch()?; + } + + query_builder + .execute_query::<PaymentIntentMetricRow, _>(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentIntentMetricsBucketIdentifier::new( + i.status.as_ref().map(|i| i.0), + i.currency.as_ref().map(|i| i.0), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => 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<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs new file mode 100644 index 000000000000..1e69d9e0996d --- /dev/null +++ b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs @@ -0,0 +1,127 @@ +use api_models::analytics::{ + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentIntentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct SuccessfulSmartRetries; + +#[async_trait::async_trait] +impl<T> super::PaymentIntentMetric<T> for SuccessfulSmartRetries +where + T: AnalyticsDataSource + super::PaymentIntentMetricAnalytics, + 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: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + let mut query_builder: QueryBuilder<T> = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); + + 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()?; + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .switch()?; + query_builder + .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .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::<PaymentIntentMetricRow, _>(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentIntentMetricsBucketIdentifier::new( + i.status.as_ref().map(|i| i.0), + i.currency.as_ref().map(|i| i.0), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => 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<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs new file mode 100644 index 000000000000..d5a3d142baf7 --- /dev/null +++ b/crates/analytics/src/payment_intents/metrics/total_smart_retries.rs @@ -0,0 +1,125 @@ +use api_models::analytics::{ + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, + Granularity, TimeRange, +}; +use common_utils::errors::ReportSwitchExt; +use error_stack::ResultExt; +use time::PrimitiveDateTime; + +use super::PaymentIntentMetricRow; +use crate::{ + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, + types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, +}; + +#[derive(Default)] +pub(super) struct TotalSmartRetries; + +#[async_trait::async_trait] +impl<T> super::PaymentIntentMetric<T> for TotalSmartRetries +where + T: AnalyticsDataSource + super::PaymentIntentMetricAnalytics, + 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: &[PaymentIntentDimensions], + merchant_id: &str, + filters: &PaymentIntentFilters, + granularity: &Option<Granularity>, + time_range: &TimeRange, + pool: &T, + ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + let mut query_builder: QueryBuilder<T> = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); + + 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()?; + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .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::<PaymentIntentMetricRow, _>(pool) + .await + .change_context(MetricsError::QueryBuildingError)? + .change_context(MetricsError::QueryExecutionFailure)? + .into_iter() + .map(|i| { + Ok(( + PaymentIntentMetricsBucketIdentifier::new( + i.status.as_ref().map(|i| i.0), + i.currency.as_ref().map(|i| i.0), + TimeRange { + start_time: match (granularity, i.start_bucket) { + (Some(g), Some(st)) => g.clip_to_start(st)?, + _ => 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<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>, + crate::query::PostProcessingError, + >>() + .change_context(MetricsError::PostProcessingFailure) + } +} diff --git a/crates/analytics/src/payment_intents/types.rs b/crates/analytics/src/payment_intents/types.rs new file mode 100644 index 000000000000..9a3556432b52 --- /dev/null +++ b/crates/analytics/src/payment_intents/types.rs @@ -0,0 +1,28 @@ +use api_models::analytics::payment_intents::{PaymentIntentDimensions, PaymentIntentFilters}; +use error_stack::ResultExt; + +use crate::{ + query::{QueryBuilder, QueryFilter, QueryResult, ToSql}, + types::{AnalyticsCollection, AnalyticsDataSource}, +}; + +impl<T> QueryFilter<T> for PaymentIntentFilters +where + T: AnalyticsDataSource, + AnalyticsCollection: ToSql<T>, +{ + fn set_filter_clause(&self, builder: &mut QueryBuilder<T>) -> QueryResult<()> { + + if !self.status.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::PaymentIntentStatus, &self.status) + .attach_printable("Error adding payment intent status filter")?; + } + if !self.currency.is_empty() { + builder + .add_filter_in_range_clause(PaymentIntentDimensions::Currency, &self.currency) + .attach_printable("Error adding currency filter")?; + } + Ok(()) + } +} diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index 2fda8fc57cdf..caeff0997d84 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -7,13 +7,14 @@ use api_models::{ auth_events::AuthEventFlows, disputes::DisputeDimensions, payments::{PaymentDimensions, PaymentDistributions}, + payment_intents::PaymentIntentDimensions, refunds::{RefundDimensions, RefundType}, sdk_events::{SdkEventDimensions, SdkEventNames}, Granularity, }, enums::{ AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, PaymentMethod, - PaymentMethodType, + PaymentMethodType, IntentStatus }, refunds::RefundStatus, }; @@ -369,8 +370,10 @@ impl_to_sql_for_to_string!( String, &str, &PaymentDimensions, + &PaymentIntentDimensions, &RefundDimensions, PaymentDimensions, + PaymentIntentDimensions, &PaymentDistributions, RefundDimensions, PaymentMethod, @@ -378,6 +381,7 @@ impl_to_sql_for_to_string!( AuthenticationType, Connector, AttemptStatus, + IntentStatus, RefundStatus, storage_enums::RefundStatus, Currency, diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 76ad9c254be2..664471d15054 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -9,7 +9,7 @@ use common_utils::{ DbConnectionParams, }; use diesel_models::enums::{ - AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, + AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, IntentStatus, }; use error_stack::ResultExt; use sqlx::{ @@ -87,6 +87,7 @@ macro_rules! db_type { db_type!(Currency); db_type!(AuthenticationType); db_type!(AttemptStatus); +db_type!(IntentStatus); db_type!(PaymentMethod, TEXT); db_type!(RefundStatus); db_type!(RefundType); @@ -143,6 +144,8 @@ where impl super::payments::filters::PaymentFilterAnalytics for SqlxClient {} impl super::payments::metrics::PaymentMetricAnalytics for SqlxClient {} impl super::payments::distribution::PaymentDistributionAnalytics for SqlxClient {} +impl super::payment_intents::filters::PaymentIntentFilterAnalytics for SqlxClient {} +impl super::payment_intents::metrics::PaymentIntentMetricAnalytics for SqlxClient {} impl super::refunds::metrics::RefundMetricAnalytics for SqlxClient {} impl super::refunds::filters::RefundFilterAnalytics for SqlxClient {} impl super::disputes::filters::DisputeFilterAnalytics for SqlxClient {} @@ -429,6 +432,63 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::FilterRow { } } +impl<'a> FromRow<'a, PgRow> for super::payment_intents::metrics::PaymentIntentMetricRow { + fn from_row(row: &'a PgRow) -> sqlx::Result<Self> { + let status: Option<DBEnumWrapper<IntentStatus>> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let currency: Option<DBEnumWrapper<Currency>> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let total: Option<bigdecimal::BigDecimal> = row.try_get("total").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let count: Option<i64> = row.try_get("count").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + // Removing millisecond precision to get accurate diffs against clickhouse + let start_bucket: Option<PrimitiveDateTime> = row + .try_get::<Option<PrimitiveDateTime>, _>("start_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + let end_bucket: Option<PrimitiveDateTime> = row + .try_get::<Option<PrimitiveDateTime>, _>("end_bucket")? + .and_then(|dt| dt.replace_millisecond(0).ok()); + Ok(Self { + status, + currency, + total, + count, + start_bucket, + end_bucket, + }) + } +} + +impl<'a> FromRow<'a, PgRow> for super::payment_intents::filters::PaymentIntentFilterRow { + fn from_row(row: &'a PgRow) -> sqlx::Result<Self> { + let status: Option<DBEnumWrapper<IntentStatus>> = + row.try_get("status").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + let currency: Option<DBEnumWrapper<Currency>> = + row.try_get("currency").or_else(|e| match e { + ColumnNotFound(_) => Ok(Default::default()), + e => Err(e), + })?; + Ok(Self { + status, + currency, + }) + } +} + impl<'a> FromRow<'a, PgRow> for super::refunds::filters::RefundFilterRow { fn from_row(row: &'a PgRow) -> sqlx::Result<Self> { let currency: Option<DBEnumWrapper<Currency>> = diff --git a/crates/analytics/src/types.rs b/crates/analytics/src/types.rs index 5370fbc25ac4..816c77fd3049 100644 --- a/crates/analytics/src/types.rs +++ b/crates/analytics/src/types.rs @@ -15,6 +15,7 @@ use crate::errors::AnalyticsError; pub enum AnalyticsDomain { Payments, Refunds, + PaymentIntents, AuthEvents, SdkEvents, ApiEvents, diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 0afe9bd6c5e3..8f853172ae71 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -3,6 +3,7 @@ use api_models::analytics::{ auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, payments::{PaymentDimensions, PaymentMetrics}, + payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics}, refunds::{RefundDimensions, RefundMetrics}, sdk_events::{SdkEventDimensions, SdkEventMetrics}, NameDescription, @@ -13,6 +14,10 @@ pub fn get_payment_dimensions() -> Vec<NameDescription> { PaymentDimensions::iter().map(Into::into).collect() } +pub fn get_payment_intent_dimensions() -> Vec<NameDescription> { + PaymentIntentDimensions::iter().map(Into::into).collect() +} + pub fn get_refund_dimensions() -> Vec<NameDescription> { RefundDimensions::iter().map(Into::into).collect() } @@ -29,6 +34,10 @@ pub fn get_payment_metrics_info() -> Vec<NameDescription> { PaymentMetrics::iter().map(Into::into).collect() } +pub fn get_payment_intent_metrics_info() -> Vec<NameDescription> { + PaymentIntentMetrics::iter().map(Into::into).collect() +} + pub fn get_refund_metrics_info() -> Vec<NameDescription> { RefundMetrics::iter().map(Into::into).collect() } diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 491420cfc028..791ef83bfdcb 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -9,6 +9,7 @@ use self::{ auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, + payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics}, refunds::{RefundDimensions, RefundMetrics}, sdk_events::{SdkEventDimensions, SdkEventMetrics}, }; @@ -21,6 +22,7 @@ pub mod connector_events; pub mod disputes; pub mod outgoing_webhook_event; pub mod payments; +pub mod payment_intents; pub mod refunds; pub mod sdk_events; pub mod search; @@ -114,6 +116,20 @@ pub struct GenerateReportRequest { pub email: Secret<String, EmailStrategy>, } +#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentIntentMetricRequest { + pub time_series: Option<TimeSeries>, + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec<PaymentIntentDimensions>, + #[serde(default)] + pub filters: payment_intents::PaymentIntentFilters, + pub metrics: HashSet<PaymentIntentMetrics>, + #[serde(default)] + pub delta: bool, +} + #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetRefundMetricRequest { @@ -187,6 +203,27 @@ pub struct FilterValue { pub values: Vec<String>, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GetPaymentIntentFiltersRequest { + pub time_range: TimeRange, + #[serde(default)] + pub group_by_names: Vec<PaymentIntentDimensions>, +} + +#[derive(Debug, Default, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentIntentFiltersResponse { + pub query_data: Vec<PaymentIntentFilterValue>, +} + +#[derive(Debug, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct PaymentIntentFilterValue { + pub dimension: PaymentIntentDimensions, + pub values: Vec<String>, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs new file mode 100644 index 000000000000..2d6ae843752b --- /dev/null +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -0,0 +1,151 @@ +use std::{ + collections::hash_map::DefaultHasher, + hash::{Hash, Hasher}, +}; + +use super::{NameDescription, TimeRange}; +use crate::enums::{IntentStatus, Currency}; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct PaymentIntentFilters { + #[serde(default)] + pub status: Vec<IntentStatus>, + pub currency: Vec<Currency>, +} + +#[derive( + Debug, + serde::Serialize, + serde::Deserialize, + strum::AsRefStr, + PartialEq, + PartialOrd, + Eq, + Ord, + strum::Display, + strum::EnumIter, + Clone, + Copy, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum PaymentIntentDimensions { + #[strum(serialize = "status")] + #[serde(rename = "status")] + PaymentIntentStatus, + Currency, +} + +#[derive( + Clone, + Debug, + Hash, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, + strum::Display, + strum::EnumIter, + strum::AsRefStr, +)] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum PaymentIntentMetrics { + SuccessfulSmartRetries, + TotalSmartRetries, + SmartRetriedAmount, + PaymentIntentCount, +} + +#[derive(Debug, Default, serde::Serialize)] +pub struct ErrorResult { + pub reason: String, + pub count: i64, + pub percentage: f64, +} + +pub mod metric_behaviour { + pub struct SuccessfulSmartRetries; + pub struct TotalSmartRetries; + pub struct SmartRetriedAmount; + pub struct PaymentIntentCount; +} + +impl From<PaymentIntentMetrics> for NameDescription { + fn from(value: PaymentIntentMetrics) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +impl From<PaymentIntentDimensions> for NameDescription { + fn from(value: PaymentIntentDimensions) -> Self { + Self { + name: value.to_string(), + desc: String::new(), + } + } +} + +#[derive(Debug, serde::Serialize, Eq)] +pub struct PaymentIntentMetricsBucketIdentifier { + pub status: Option<IntentStatus>, + pub currency: Option<Currency>, + #[serde(rename = "time_range")] + pub time_bucket: TimeRange, + #[serde(rename = "time_bucket")] + #[serde(with = "common_utils::custom_serde::iso8601custom")] + pub start_time: time::PrimitiveDateTime, +} + +impl PaymentIntentMetricsBucketIdentifier { + #[allow(clippy::too_many_arguments)] + pub fn new( + status: Option<IntentStatus>, + currency: Option<Currency>, + normalized_time_range: TimeRange, + ) -> Self { + Self { + status, + currency, + time_bucket: normalized_time_range, + start_time: normalized_time_range.start_time, + } + } +} + +impl Hash for PaymentIntentMetricsBucketIdentifier { + fn hash<H: Hasher>(&self, state: &mut H) { + self.status.map(|i| i.to_string()).hash(state); + self.currency.hash(state); + self.time_bucket.hash(state); + } +} + +impl PartialEq for PaymentIntentMetricsBucketIdentifier { + fn eq(&self, other: &Self) -> bool { + let mut left = DefaultHasher::new(); + self.hash(&mut left); + let mut right = DefaultHasher::new(); + other.hash(&mut right); + left.finish() == right.finish() + } +} + +#[derive(Debug, serde::Serialize)] +pub struct PaymentIntentMetricsBucketValue { + pub successful_smart_retries: Option<u64>, + pub total_smart_retries: Option<u64>, + pub smart_retried_amount: Option<u64>, + pub payment_intent_count: Option<u64>, +} + +#[derive(Debug, serde::Serialize)] +pub struct MetricsBucketResponse { + #[serde(flatten)] + pub values: PaymentIntentMetricsBucketValue, + #[serde(flatten)] + pub dimensions: PaymentIntentMetricsBucketIdentifier, +} diff --git a/crates/api_models/src/events.rs b/crates/api_models/src/events.rs index bed46f01f192..346fc06f20a5 100644 --- a/crates/api_models/src/events.rs +++ b/crates/api_models/src/events.rs @@ -38,6 +38,24 @@ use crate::{ impl ApiEventMetric for TimeRange {} +impl ApiEventMetric for GetPaymentIntentFiltersRequest { + fn get_api_event_type(&self) -> Option<ApiEventsType> { + Some(ApiEventsType::Analytics) + } +} + +impl ApiEventMetric for GetPaymentIntentMetricRequest { + fn get_api_event_type(&self) -> Option<ApiEventsType> { + Some(ApiEventsType::Analytics) + } +} + +impl ApiEventMetric for PaymentIntentFiltersResponse { + fn get_api_event_type(&self) -> Option<ApiEventsType> { + Some(ApiEventsType::Analytics) + } +} + impl_misc_api_event_type!( PaymentMethodId, PaymentsSessionResponse, diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 0ef8a6a8bfcf..337a82ee0971 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -65,6 +65,7 @@ pub enum ApiEventsType { Poll { poll_id: String, }, + Analytics } impl ApiEventMetric for serde_json::Value {} diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index ae3b03f7b82a..b6ee50c792c5 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -14,8 +14,9 @@ pub mod routes { }, GenerateReportRequest, GetActivePaymentsMetricRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, GetAuthEventMetricRequest, GetDisputeMetricRequest, - GetPaymentFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, - GetRefundMetricRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, + GetPaymentFiltersRequest, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, + GetPaymentMetricRequest, GetRefundFilterRequest, GetRefundMetricRequest, + GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, }; use error_stack::ResultExt; @@ -43,6 +44,10 @@ pub mod routes { web::resource("metrics/payments") .route(web::post().to(get_payment_metrics)), ) + .service( + web::resource("metrics/payment_intents") + .route(web::post().to(get_payment_intents_metrics)), + ) .service( web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics)), ) @@ -50,6 +55,10 @@ pub mod routes { web::resource("filters/payments") .route(web::post().to(get_payment_filters)), ) + .service( + web::resource("filters/payment_intents") + .route(web::post().to(get_payment_intents_filters)), + ) .service( web::resource("filters/refunds").route(web::post().to(get_refund_filters)), ) @@ -177,6 +186,42 @@ pub mod routes { .await } + /// # Panics + /// + /// Panics if `json_payload` array does not contain one `GetPaymentIntentMetricRequest` element. + pub async fn get_payment_intents_metrics( + state: web::Data<AppState>, + req: actix_web::HttpRequest, + json_payload: web::Json<[GetPaymentIntentMetricRequest; 1]>, + ) -> impl Responder { + // safety: This shouldn't panic owing to the data type + #[allow(clippy::expect_used)] + let payload = json_payload + .into_inner() + .to_vec() + .pop() + .expect("Couldn't get GetPaymentIntentMetricRequest"); + let flow = AnalyticsFlow::GetPaymentIntentMetrics; + Box::pin(api::server_wrap( + flow, + state, + &req, + payload, + |state, auth: AuthenticationData, req, _| async move { + analytics::payment_intents::get_metrics( + &state.pool, + &auth.merchant_account.merchant_id, + req, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + /// # Panics /// /// Panics if `json_payload` array does not contain one `GetRefundMetricRequest` element. @@ -349,6 +394,32 @@ pub mod routes { .await } + pub async fn get_payment_intents_filters( + state: web::Data<AppState>, + req: actix_web::HttpRequest, + json_payload: web::Json<GetPaymentIntentFiltersRequest>, + ) -> impl Responder { + let flow = AnalyticsFlow::GetPaymentIntentFilters; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, auth: AuthenticationData, req, _| async move { + analytics::payment_intents::get_filters( + &state.pool, + req, + &auth.merchant_account.merchant_id, + ) + .await + .map(ApplicationResponse::Json) + }, + &auth::JWTAuth(Permission::Analytics), + api_locking::LockAction::NotApplicable, + )) + .await + } + pub async fn get_refund_filters( state: web::Data<AppState>, req: actix_web::HttpRequest, From 73b94cdec9e06d6869bf270b820490d263991d81 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 27 Jun 2024 19:38:46 +0000 Subject: [PATCH 2/5] chore: run formatter --- crates/analytics/src/lib.rs | 12 ++++++++---- crates/analytics/src/payment_intents.rs | 4 +--- .../analytics/src/payment_intents/accumulator.rs | 1 + crates/analytics/src/payment_intents/filters.rs | 2 +- crates/analytics/src/payment_intents/metrics.rs | 14 ++++++++------ .../metrics/payment_intent_count.rs | 7 +++++-- .../metrics/smart_retried_amount.rs | 7 +++++-- .../metrics/successful_smart_retries.rs | 2 +- crates/analytics/src/payment_intents/types.rs | 8 +++++--- crates/analytics/src/query.rs | 6 +++--- crates/analytics/src/sqlx.rs | 7 ++----- crates/analytics/src/utils.rs | 2 +- crates/api_models/src/analytics.rs | 4 ++-- crates/api_models/src/analytics/payment_intents.rs | 2 +- crates/common_utils/src/events.rs | 2 +- 15 files changed, 45 insertions(+), 35 deletions(-) diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index ea70257d6812..10e628475e84 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -3,8 +3,8 @@ pub mod core; pub mod disputes; pub mod errors; pub mod metrics; -pub mod payments; pub mod payment_intents; +pub mod payments; mod query; pub mod refunds; @@ -40,8 +40,11 @@ use api_models::analytics::{ }, auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier}, disputes::{DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier}, + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, + PaymentIntentMetricsBucketIdentifier, + }, payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier}, - payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, PaymentIntentMetricsBucketIdentifier}, refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier}, sdk_events::{ SdkEventDimensions, SdkEventFilters, SdkEventMetrics, SdkEventMetricsBucketIdentifier, @@ -62,11 +65,11 @@ use strum::Display; use self::{ active_payments::metrics::{ActivePaymentsMetric, ActivePaymentsMetricRow}, auth_events::metrics::{AuthEventMetric, AuthEventMetricRow}, + payment_intents::metrics::{PaymentIntentMetric, PaymentIntentMetricRow}, payments::{ distribution::{PaymentDistribution, PaymentDistributionRow}, metrics::{PaymentMetric, PaymentMetricRow}, }, - payment_intents::metrics::{PaymentIntentMetric, PaymentIntentMetricRow}, refunds::metrics::{RefundMetric, RefundMetricRow}, sdk_events::metrics::{SdkEventMetric, SdkEventMetricRow}, sqlx::SqlxClient, @@ -324,7 +327,8 @@ impl AnalyticsProvider { filters: &PaymentIntentFilters, granularity: &Option<Granularity>, time_range: &TimeRange, - ) -> types::MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { + ) -> types::MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> + { // Metrics to get the fetch time for each payment intent metric metrics::request::record_operation_time( async { diff --git a/crates/analytics/src/payment_intents.rs b/crates/analytics/src/payment_intents.rs index 6f3066d87d48..449dd94788c3 100644 --- a/crates/analytics/src/payment_intents.rs +++ b/crates/analytics/src/payment_intents.rs @@ -3,9 +3,7 @@ mod core; pub mod filters; pub mod metrics; pub mod types; -pub use accumulator::{ - PaymentIntentMetricAccumulator, PaymentIntentMetricsAccumulator, -}; +pub use accumulator::{PaymentIntentMetricAccumulator, PaymentIntentMetricsAccumulator}; pub trait PaymentIntentAnalytics: metrics::PaymentIntentMetricAnalytics + filters::PaymentIntentFilterAnalytics diff --git a/crates/analytics/src/payment_intents/accumulator.rs b/crates/analytics/src/payment_intents/accumulator.rs index 00b89fb90e7e..8fd98a1e73cc 100644 --- a/crates/analytics/src/payment_intents/accumulator.rs +++ b/crates/analytics/src/payment_intents/accumulator.rs @@ -1,5 +1,6 @@ use api_models::analytics::payment_intents::PaymentIntentMetricsBucketValue; use bigdecimal::ToPrimitive; + use super::metrics::PaymentIntentMetricRow; #[derive(Debug, Default)] diff --git a/crates/analytics/src/payment_intents/filters.rs b/crates/analytics/src/payment_intents/filters.rs index bea846214799..1a74cfd510e9 100644 --- a/crates/analytics/src/payment_intents/filters.rs +++ b/crates/analytics/src/payment_intents/filters.rs @@ -1,6 +1,6 @@ use api_models::analytics::{payment_intents::PaymentIntentDimensions, Granularity, TimeRange}; use common_utils::errors::ReportSwitchExt; -use diesel_models::enums::{IntentStatus, Currency}; +use diesel_models::enums::{Currency, IntentStatus}; use error_stack::ResultExt; use time::PrimitiveDateTime; diff --git a/crates/analytics/src/payment_intents/metrics.rs b/crates/analytics/src/payment_intents/metrics.rs index 7e9990166f47..3a0cbbc85db0 100644 --- a/crates/analytics/src/payment_intents/metrics.rs +++ b/crates/analytics/src/payment_intents/metrics.rs @@ -1,5 +1,8 @@ use api_models::analytics::{ - payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, PaymentIntentMetricsBucketIdentifier}, + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics, + PaymentIntentMetricsBucketIdentifier, + }, Granularity, TimeRange, }; use diesel_models::enums as storage_enums; @@ -10,16 +13,15 @@ use crate::{ types::{AnalyticsCollection, AnalyticsDataSource, DBEnumWrapper, LoadRow, MetricsResult}, }; +mod payment_intent_count; +mod smart_retried_amount; mod successful_smart_retries; mod total_smart_retries; -mod smart_retried_amount; -mod payment_intent_count; +use payment_intent_count::PaymentIntentCount; +use smart_retried_amount::SmartRetriedAmount; use successful_smart_retries::SuccessfulSmartRetries; use total_smart_retries::TotalSmartRetries; -use smart_retried_amount::SmartRetriedAmount; -use payment_intent_count::PaymentIntentCount; - #[derive(Debug, PartialEq, Eq, serde::Deserialize)] pub struct PaymentIntentMetricRow { diff --git a/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs index 5a1bc5edd8c4..0f235375c4f8 100644 --- a/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs +++ b/crates/analytics/src/payment_intents/metrics/payment_intent_count.rs @@ -1,5 +1,7 @@ use api_models::analytics::{ - payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier}, + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; @@ -34,7 +36,8 @@ where time_range: &TimeRange, pool: &T, ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { - let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::PaymentIntent); + let mut query_builder: QueryBuilder<T> = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); for dim in dimensions.iter() { query_builder.add_select_column(dim).switch()?; diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index a25d29fd8bf2..a0c9526c31c7 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -1,5 +1,7 @@ use api_models::analytics::{ - payment_intents::{PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier}, + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, Granularity, TimeRange, }; use common_utils::errors::ReportSwitchExt; @@ -34,7 +36,8 @@ where time_range: &TimeRange, pool: &T, ) -> MetricsResult<Vec<(PaymentIntentMetricsBucketIdentifier, PaymentIntentMetricRow)>> { - let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::PaymentIntent); + let mut query_builder: QueryBuilder<T> = + QueryBuilder::new(AnalyticsCollection::PaymentIntent); for dim in dimensions.iter() { query_builder.add_select_column(dim).switch()?; diff --git a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs index 1e69d9e0996d..a585710628ee 100644 --- a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs @@ -92,7 +92,7 @@ where .attach_printable("Error adding granularity") .switch()?; } - + query_builder .execute_query::<PaymentIntentMetricRow, _>(pool) .await diff --git a/crates/analytics/src/payment_intents/types.rs b/crates/analytics/src/payment_intents/types.rs index 9a3556432b52..9b1e7b8674d1 100644 --- a/crates/analytics/src/payment_intents/types.rs +++ b/crates/analytics/src/payment_intents/types.rs @@ -12,11 +12,13 @@ where AnalyticsCollection: ToSql<T>, { fn set_filter_clause(&self, builder: &mut QueryBuilder<T>) -> QueryResult<()> { - if !self.status.is_empty() { builder - .add_filter_in_range_clause(PaymentIntentDimensions::PaymentIntentStatus, &self.status) - .attach_printable("Error adding payment intent status filter")?; + .add_filter_in_range_clause( + PaymentIntentDimensions::PaymentIntentStatus, + &self.status, + ) + .attach_printable("Error adding payment intent status filter")?; } if !self.currency.is_empty() { builder diff --git a/crates/analytics/src/query.rs b/crates/analytics/src/query.rs index caeff0997d84..a257fedc09dd 100644 --- a/crates/analytics/src/query.rs +++ b/crates/analytics/src/query.rs @@ -6,15 +6,15 @@ use api_models::{ api_event::ApiEventDimensions, auth_events::AuthEventFlows, disputes::DisputeDimensions, - payments::{PaymentDimensions, PaymentDistributions}, payment_intents::PaymentIntentDimensions, + payments::{PaymentDimensions, PaymentDistributions}, refunds::{RefundDimensions, RefundType}, sdk_events::{SdkEventDimensions, SdkEventNames}, Granularity, }, enums::{ - AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, PaymentMethod, - PaymentMethodType, IntentStatus + AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, IntentStatus, + PaymentMethod, PaymentMethodType, }, refunds::RefundStatus, }; diff --git a/crates/analytics/src/sqlx.rs b/crates/analytics/src/sqlx.rs index 664471d15054..6a4faf50eb86 100644 --- a/crates/analytics/src/sqlx.rs +++ b/crates/analytics/src/sqlx.rs @@ -9,7 +9,7 @@ use common_utils::{ DbConnectionParams, }; use diesel_models::enums::{ - AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus, IntentStatus, + AttemptStatus, AuthenticationType, Currency, IntentStatus, PaymentMethod, RefundStatus, }; use error_stack::ResultExt; use sqlx::{ @@ -482,10 +482,7 @@ impl<'a> FromRow<'a, PgRow> for super::payment_intents::filters::PaymentIntentFi ColumnNotFound(_) => Ok(Default::default()), e => Err(e), })?; - Ok(Self { - status, - currency, - }) + Ok(Self { status, currency }) } } diff --git a/crates/analytics/src/utils.rs b/crates/analytics/src/utils.rs index 8f853172ae71..3955a8c1dfe1 100644 --- a/crates/analytics/src/utils.rs +++ b/crates/analytics/src/utils.rs @@ -2,8 +2,8 @@ use api_models::analytics::{ api_event::{ApiEventDimensions, ApiEventMetrics}, auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, - payments::{PaymentDimensions, PaymentMetrics}, payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics}, + payments::{PaymentDimensions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, sdk_events::{SdkEventDimensions, SdkEventMetrics}, NameDescription, diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 791ef83bfdcb..85a9c3ded09d 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -8,8 +8,8 @@ use self::{ api_event::{ApiEventDimensions, ApiEventMetrics}, auth_events::AuthEventMetrics, disputes::{DisputeDimensions, DisputeMetrics}, - payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics}, + payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics}, refunds::{RefundDimensions, RefundMetrics}, sdk_events::{SdkEventDimensions, SdkEventMetrics}, }; @@ -21,8 +21,8 @@ pub mod auth_events; pub mod connector_events; pub mod disputes; pub mod outgoing_webhook_event; -pub mod payments; pub mod payment_intents; +pub mod payments; pub mod refunds; pub mod sdk_events; pub mod search; diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 2d6ae843752b..232c1719047f 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -4,7 +4,7 @@ use std::{ }; use super::{NameDescription, TimeRange}; -use crate::enums::{IntentStatus, Currency}; +use crate::enums::{Currency, IntentStatus}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct PaymentIntentFilters { diff --git a/crates/common_utils/src/events.rs b/crates/common_utils/src/events.rs index 337a82ee0971..3e3a0da4cabb 100644 --- a/crates/common_utils/src/events.rs +++ b/crates/common_utils/src/events.rs @@ -65,7 +65,7 @@ pub enum ApiEventsType { Poll { poll_id: String, }, - Analytics + Analytics, } impl ApiEventMetric for serde_json::Value {} From b48c5d75c4eb77017f0a4b0a77650c5efefd931a Mon Sep 17 00:00:00 2001 From: T Sandeep Kumar <sandeep.kumar@juspay.in> Date: Fri, 28 Jun 2024 12:28:01 +0530 Subject: [PATCH 3/5] separated analytics/v1 routes from analytics/v2 routes --- crates/router/src/analytics.rs | 187 +++++++++++++++++---------------- 1 file changed, 99 insertions(+), 88 deletions(-) diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d4a3bbca71d6..64f62f487624 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -38,94 +38,105 @@ pub mod routes { impl Analytics { pub fn server(state: AppState) -> Scope { - let mut route = web::scope("/analytics/v1").app_data(web::Data::new(state)); - { - route = route - .service( - web::resource("metrics/payments") - .route(web::post().to(get_payment_metrics)), - ) - .service( - web::resource("metrics/payment_intents") - .route(web::post().to(get_payment_intents_metrics)), - ) - .service( - web::resource("metrics/refunds").route(web::post().to(get_refunds_metrics)), - ) - .service( - web::resource("filters/payments") - .route(web::post().to(get_payment_filters)), - ) - .service( - web::resource("filters/payment_intents") - .route(web::post().to(get_payment_intents_filters)), - ) - .service( - web::resource("filters/refunds").route(web::post().to(get_refund_filters)), - ) - .service(web::resource("{domain}/info").route(web::get().to(get_info))) - .service( - web::resource("report/dispute") - .route(web::post().to(generate_dispute_report)), - ) - .service( - web::resource("report/refunds") - .route(web::post().to(generate_refund_report)), - ) - .service( - web::resource("report/payments") - .route(web::post().to(generate_payment_report)), - ) - .service( - web::resource("metrics/sdk_events") - .route(web::post().to(get_sdk_event_metrics)), - ) - .service( - web::resource("metrics/active_payments") - .route(web::post().to(get_active_payments_metrics)), - ) - .service( - web::resource("filters/sdk_events") - .route(web::post().to(get_sdk_event_filters)), - ) - .service( - web::resource("metrics/auth_events") - .route(web::post().to(get_auth_event_metrics)), - ) - .service(web::resource("api_event_logs").route(web::get().to(get_api_events))) - .service(web::resource("sdk_event_logs").route(web::post().to(get_sdk_events))) - .service( - web::resource("connector_event_logs") - .route(web::get().to(get_connector_events)), - ) - .service( - web::resource("outgoing_webhook_event_logs") - .route(web::get().to(get_outgoing_webhook_events)), - ) - .service( - web::resource("filters/api_events") - .route(web::post().to(get_api_event_filters)), - ) - .service( - web::resource("metrics/api_events") - .route(web::post().to(get_api_events_metrics)), - ) - .service( - web::resource("search").route(web::post().to(get_global_search_results)), - ) - .service( - web::resource("search/{domain}").route(web::post().to(get_search_results)), - ) - .service( - web::resource("filters/disputes") - .route(web::post().to(get_dispute_filters)), - ) - .service( - web::resource("metrics/disputes") - .route(web::post().to(get_dispute_metrics)), - ) - } - route + web::scope("/analytics") + .app_data(web::Data::new(state)) + .service( + web::scope("/v1") + .service( + web::resource("metrics/payments") + .route(web::post().to(get_payment_metrics)), + ) + .service( + web::resource("metrics/refunds") + .route(web::post().to(get_refunds_metrics)), + ) + .service( + web::resource("filters/payments") + .route(web::post().to(get_payment_filters)), + ) + .service( + web::resource("filters/refunds") + .route(web::post().to(get_refund_filters)), + ) + .service(web::resource("{domain}/info").route(web::get().to(get_info))) + .service( + web::resource("report/dispute") + .route(web::post().to(generate_dispute_report)), + ) + .service( + web::resource("report/refunds") + .route(web::post().to(generate_refund_report)), + ) + .service( + web::resource("report/payments") + .route(web::post().to(generate_payment_report)), + ) + .service( + web::resource("metrics/sdk_events") + .route(web::post().to(get_sdk_event_metrics)), + ) + .service( + web::resource("metrics/active_payments") + .route(web::post().to(get_active_payments_metrics)), + ) + .service( + web::resource("filters/sdk_events") + .route(web::post().to(get_sdk_event_filters)), + ) + .service( + web::resource("metrics/auth_events") + .route(web::post().to(get_auth_event_metrics)), + ) + .service( + web::resource("api_event_logs").route(web::get().to(get_api_events)), + ) + .service( + web::resource("sdk_event_logs").route(web::post().to(get_sdk_events)), + ) + .service( + web::resource("connector_event_logs") + .route(web::get().to(get_connector_events)), + ) + .service( + web::resource("outgoing_webhook_event_logs") + .route(web::get().to(get_outgoing_webhook_events)), + ) + .service( + web::resource("filters/api_events") + .route(web::post().to(get_api_event_filters)), + ) + .service( + web::resource("metrics/api_events") + .route(web::post().to(get_api_events_metrics)), + ) + .service( + web::resource("search") + .route(web::post().to(get_global_search_results)), + ) + .service( + web::resource("search/{domain}") + .route(web::post().to(get_search_results)), + ) + .service( + web::resource("filters/disputes") + .route(web::post().to(get_dispute_filters)), + ) + .service( + web::resource("metrics/disputes") + .route(web::post().to(get_dispute_metrics)), + ), + ) + .service( + web::scope("/v2") + .service( + web::resource("/metrics/payments") + .route(web::post().to(get_payment_intents_metrics)), + ) + .service( + web::resource("/filters/payments") + .route(web::post().to(get_payment_intents_filters)), + ), + ) } } From 9c21ec1624fa734b5112b95504278b7640678e67 Mon Sep 17 00:00:00 2001 From: T Sandeep Kumar <sandeep.kumar@juspay.in> Date: Mon, 1 Jul 2024 12:56:51 +0530 Subject: [PATCH 4/5] Resolved comments --- .../payment_intents/metrics/smart_retried_amount.rs | 13 +++++++++---- .../metrics/successful_smart_retries.rs | 6 +++--- .../analytics/src/payments/metrics/retries_count.rs | 6 +++--- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index a0c9526c31c7..2c0bb1e43981 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -1,16 +1,16 @@ -use api_models::analytics::{ +use api_models::{analytics::{ payment_intents::{ PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, }, Granularity, TimeRange, -}; +}, enums::IntentStatus}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentIntentMetricRow; use crate::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window}, + query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window, FilterTypes}, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; @@ -66,7 +66,12 @@ where query_builder .add_filter_clause("merchant_id", merchant_id) .switch()?; - + query_builder + .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) + .switch()?; + query_builder + .add_custom_filter_clause("status", IntentStatus::Succeeded, FilterTypes::Equal) + .switch()?; time_range .set_filter_clause(&mut query_builder) .attach_printable("Error filtering time range") diff --git a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs index a585710628ee..a2f22e2fe631 100644 --- a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs @@ -1,9 +1,9 @@ -use api_models::analytics::{ +use api_models::{analytics::{ payment_intents::{ PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, }, Granularity, TimeRange, -}; +}, enums::IntentStatus}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; @@ -73,7 +73,7 @@ where .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) .switch()?; query_builder - .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .add_custom_filter_clause("status", IntentStatus::Succeeded, FilterTypes::Equal) .switch()?; time_range .set_filter_clause(&mut query_builder) diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs index 87d80c87fb4d..49476aa3e95e 100644 --- a/crates/analytics/src/payments/metrics/retries_count.rs +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -1,7 +1,7 @@ -use api_models::analytics::{ +use api_models::{analytics::{ payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, Granularity, TimeRange, -}; +}, enums::IntentStatus}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; @@ -70,7 +70,7 @@ where .add_custom_filter_clause("attempt_count", "1", FilterTypes::Gt) .switch()?; query_builder - .add_custom_filter_clause("status", "succeeded", FilterTypes::Equal) + .add_custom_filter_clause("status", IntentStatus::Succeeded, FilterTypes::Equal) .switch()?; time_range .set_filter_clause(&mut query_builder) From 021c6c3d6f7fa2d54aa979cf3ea06805e3dfd856 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 1 Jul 2024 07:27:54 +0000 Subject: [PATCH 5/5] chore: run formatter --- .../metrics/smart_retried_amount.rs | 18 ++++++++++++------ .../metrics/successful_smart_retries.rs | 13 ++++++++----- .../src/payments/metrics/retries_count.rs | 11 +++++++---- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index 2c0bb1e43981..470a0e668673 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -1,16 +1,22 @@ -use api_models::{analytics::{ - payment_intents::{ - PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, +use api_models::{ + analytics::{ + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, + Granularity, TimeRange, }, - Granularity, TimeRange, -}, enums::IntentStatus}; + enums::IntentStatus, +}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; use super::PaymentIntentMetricRow; use crate::{ - query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window, FilterTypes}, + query::{ + Aggregate, FilterTypes, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, + Window, + }, types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult}, }; diff --git a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs index a2f22e2fe631..292062d1e109 100644 --- a/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs +++ b/crates/analytics/src/payment_intents/metrics/successful_smart_retries.rs @@ -1,9 +1,12 @@ -use api_models::{analytics::{ - payment_intents::{ - PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, +use api_models::{ + analytics::{ + payment_intents::{ + PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetricsBucketIdentifier, + }, + Granularity, TimeRange, }, - Granularity, TimeRange, -}, enums::IntentStatus}; + enums::IntentStatus, +}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime; diff --git a/crates/analytics/src/payments/metrics/retries_count.rs b/crates/analytics/src/payments/metrics/retries_count.rs index 49476aa3e95e..3c4580d37a74 100644 --- a/crates/analytics/src/payments/metrics/retries_count.rs +++ b/crates/analytics/src/payments/metrics/retries_count.rs @@ -1,7 +1,10 @@ -use api_models::{analytics::{ - payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, - Granularity, TimeRange, -}, enums::IntentStatus}; +use api_models::{ + analytics::{ + payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier}, + Granularity, TimeRange, + }, + enums::IntentStatus, +}; use common_utils::errors::ReportSwitchExt; use error_stack::ResultExt; use time::PrimitiveDateTime;