mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(analytics): Add v2 payment analytics (payment-intents analytics) (#5150)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -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>;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -3,6 +3,7 @@ pub mod core;
|
||||
pub mod disputes;
|
||||
pub mod errors;
|
||||
pub mod metrics;
|
||||
pub mod payment_intents;
|
||||
pub mod payments;
|
||||
mod query;
|
||||
pub mod refunds;
|
||||
@ -39,6 +40,10 @@ use api_models::analytics::{
|
||||
},
|
||||
auth_events::{AuthEventMetrics, AuthEventMetricsBucketIdentifier},
|
||||
disputes::{DisputeDimensions, DisputeFilters, DisputeMetrics, DisputeMetricsBucketIdentifier},
|
||||
payment_intents::{
|
||||
PaymentIntentDimensions, PaymentIntentFilters, PaymentIntentMetrics,
|
||||
PaymentIntentMetricsBucketIdentifier,
|
||||
},
|
||||
payments::{PaymentDimensions, PaymentFilters, PaymentMetrics, PaymentMetricsBucketIdentifier},
|
||||
refunds::{RefundDimensions, RefundFilters, RefundMetrics, RefundMetricsBucketIdentifier},
|
||||
sdk_events::{
|
||||
@ -60,6 +65,7 @@ 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},
|
||||
@ -313,6 +319,111 @@ 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 +867,13 @@ pub struct ReportConfig {
|
||||
pub enum AnalyticsFlow {
|
||||
GetInfo,
|
||||
GetPaymentMetrics,
|
||||
GetPaymentIntentMetrics,
|
||||
GetRefundsMetrics,
|
||||
GetSdkMetrics,
|
||||
GetAuthMetrics,
|
||||
GetActivePaymentsMetrics,
|
||||
GetPaymentFilters,
|
||||
GetPaymentIntentFilters,
|
||||
GetRefundFilters,
|
||||
GetSdkEventFilters,
|
||||
GetApiEvents,
|
||||
|
||||
13
crates/analytics/src/payment_intents.rs
Normal file
13
crates/analytics/src/payment_intents.rs
Normal file
@ -0,0 +1,13 @@
|
||||
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};
|
||||
90
crates/analytics/src/payment_intents/accumulator.rs
Normal file
90
crates/analytics/src/payment_intents/accumulator.rs
Normal file
@ -0,0 +1,90 @@
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
226
crates/analytics/src/payment_intents/core.rs
Normal file
226
crates/analytics/src/payment_intents/core.rs
Normal file
@ -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)
|
||||
}
|
||||
56
crates/analytics/src/payment_intents/filters.rs
Normal file
56
crates/analytics/src/payment_intents/filters.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use api_models::analytics::{payment_intents::PaymentIntentDimensions, Granularity, TimeRange};
|
||||
use common_utils::errors::ReportSwitchExt;
|
||||
use diesel_models::enums::{Currency, IntentStatus};
|
||||
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>>,
|
||||
}
|
||||
126
crates/analytics/src/payment_intents/metrics.rs
Normal file
126
crates/analytics/src/payment_intents/metrics.rs
Normal file
@ -0,0 +1,126 @@
|
||||
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 payment_intent_count;
|
||||
mod smart_retried_amount;
|
||||
mod successful_smart_retries;
|
||||
mod total_smart_retries;
|
||||
|
||||
use payment_intent_count::PaymentIntentCount;
|
||||
use smart_retried_amount::SmartRetriedAmount;
|
||||
use successful_smart_retries::SuccessfulSmartRetries;
|
||||
use total_smart_retries::TotalSmartRetries;
|
||||
|
||||
#[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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
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, FilterTypes, 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()?;
|
||||
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")
|
||||
.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)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
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, 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", IntentStatus::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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
30
crates/analytics/src/payment_intents/types.rs
Normal file
30
crates/analytics/src/payment_intents/types.rs
Normal file
@ -0,0 +1,30 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,9 @@
|
||||
use api_models::analytics::{
|
||||
payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier},
|
||||
Granularity, TimeRange,
|
||||
use api_models::{
|
||||
analytics::{
|
||||
payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier},
|
||||
Granularity, TimeRange,
|
||||
},
|
||||
enums::IntentStatus,
|
||||
};
|
||||
use common_utils::errors::ReportSwitchExt;
|
||||
use error_stack::ResultExt;
|
||||
@ -70,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)
|
||||
|
||||
@ -6,14 +6,15 @@ use api_models::{
|
||||
api_event::ApiEventDimensions,
|
||||
auth_events::AuthEventFlows,
|
||||
disputes::DisputeDimensions,
|
||||
payment_intents::PaymentIntentDimensions,
|
||||
payments::{PaymentDimensions, PaymentDistributions},
|
||||
refunds::{RefundDimensions, RefundType},
|
||||
sdk_events::{SdkEventDimensions, SdkEventNames},
|
||||
Granularity,
|
||||
},
|
||||
enums::{
|
||||
AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, PaymentMethod,
|
||||
PaymentMethodType,
|
||||
AttemptStatus, AuthenticationType, Connector, Currency, DisputeStage, IntentStatus,
|
||||
PaymentMethod, PaymentMethodType,
|
||||
},
|
||||
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,
|
||||
|
||||
@ -9,7 +9,7 @@ use common_utils::{
|
||||
DbConnectionParams,
|
||||
};
|
||||
use diesel_models::enums::{
|
||||
AttemptStatus, AuthenticationType, Currency, PaymentMethod, RefundStatus,
|
||||
AttemptStatus, AuthenticationType, Currency, IntentStatus, PaymentMethod, RefundStatus,
|
||||
};
|
||||
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,60 @@ 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>> =
|
||||
|
||||
@ -15,6 +15,7 @@ use crate::errors::AnalyticsError;
|
||||
pub enum AnalyticsDomain {
|
||||
Payments,
|
||||
Refunds,
|
||||
PaymentIntents,
|
||||
AuthEvents,
|
||||
SdkEvents,
|
||||
ApiEvents,
|
||||
|
||||
@ -2,6 +2,7 @@ use api_models::analytics::{
|
||||
api_event::{ApiEventDimensions, ApiEventMetrics},
|
||||
auth_events::AuthEventMetrics,
|
||||
disputes::{DisputeDimensions, DisputeMetrics},
|
||||
payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics},
|
||||
payments::{PaymentDimensions, PaymentMetrics},
|
||||
refunds::{RefundDimensions, RefundMetrics},
|
||||
sdk_events::{SdkEventDimensions, SdkEventMetrics},
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ use self::{
|
||||
api_event::{ApiEventDimensions, ApiEventMetrics},
|
||||
auth_events::AuthEventMetrics,
|
||||
disputes::{DisputeDimensions, DisputeMetrics},
|
||||
payment_intents::{PaymentIntentDimensions, PaymentIntentMetrics},
|
||||
payments::{PaymentDimensions, PaymentDistributions, PaymentMetrics},
|
||||
refunds::{RefundDimensions, RefundMetrics},
|
||||
sdk_events::{SdkEventDimensions, SdkEventMetrics},
|
||||
@ -20,6 +21,7 @@ pub mod auth_events;
|
||||
pub mod connector_events;
|
||||
pub mod disputes;
|
||||
pub mod outgoing_webhook_event;
|
||||
pub mod payment_intents;
|
||||
pub mod payments;
|
||||
pub mod refunds;
|
||||
pub mod sdk_events;
|
||||
@ -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")]
|
||||
|
||||
|
||||
151
crates/api_models/src/analytics/payment_intents.rs
Normal file
151
crates/api_models/src/analytics/payment_intents.rs
Normal file
@ -0,0 +1,151 @@
|
||||
use std::{
|
||||
collections::hash_map::DefaultHasher,
|
||||
hash::{Hash, Hasher},
|
||||
};
|
||||
|
||||
use super::{NameDescription, TimeRange};
|
||||
use crate::enums::{Currency, IntentStatus};
|
||||
|
||||
#[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,
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -65,6 +65,7 @@ pub enum ApiEventsType {
|
||||
Poll {
|
||||
poll_id: String,
|
||||
},
|
||||
Analytics,
|
||||
}
|
||||
|
||||
impl ApiEventMetric for serde_json::Value {}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -37,86 +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/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)),
|
||||
)
|
||||
}
|
||||
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)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,6 +198,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.
|
||||
@ -350,6 +406,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,
|
||||
|
||||
Reference in New Issue
Block a user