mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-27 03:13:56 +08:00
feat(analytics): implement currency conversion to power multi-currency aggregation (#6418)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@ -353,6 +353,7 @@ dependencies = [
|
||||
"bigdecimal",
|
||||
"common_enums",
|
||||
"common_utils",
|
||||
"currency_conversion",
|
||||
"diesel_models",
|
||||
"error-stack",
|
||||
"futures 0.3.30",
|
||||
@ -363,6 +364,7 @@ dependencies = [
|
||||
"opensearch",
|
||||
"reqwest 0.11.27",
|
||||
"router_env",
|
||||
"rust_decimal",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
||||
@ -21,6 +21,7 @@ hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces"
|
||||
masking = { version = "0.1.0", path = "../masking" }
|
||||
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
|
||||
storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false }
|
||||
currency_conversion = { version = "0.1.0", path = "../currency_conversion" }
|
||||
|
||||
#Third Party dependencies
|
||||
actix-web = "4.5.1"
|
||||
@ -34,6 +35,7 @@ futures = "0.3.30"
|
||||
once_cell = "1.19.0"
|
||||
opensearch = { version = "2.2.0", features = ["aws-auth"] }
|
||||
reqwest = { version = "0.11.27", features = ["serde_json"] }
|
||||
rust_decimal = "1.35"
|
||||
serde = { version = "1.0.197", features = ["derive", "rc"] }
|
||||
serde_json = "1.0.115"
|
||||
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "bigdecimal"] }
|
||||
|
||||
@ -12,6 +12,8 @@ pub enum AnalyticsError {
|
||||
UnknownError,
|
||||
#[error("Access Forbidden Analytics Error")]
|
||||
AccessForbiddenError,
|
||||
#[error("Failed to fetch currency exchange rate")]
|
||||
ForexFetchFailed,
|
||||
}
|
||||
|
||||
impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
|
||||
@ -32,6 +34,12 @@ impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
|
||||
Self::AccessForbiddenError => {
|
||||
ApiErrorResponse::Unauthorized(ApiError::new("IR", 0, "Access Forbidden", None))
|
||||
}
|
||||
Self::ForexFetchFailed => ApiErrorResponse::InternalServerError(ApiError::new(
|
||||
"HE",
|
||||
0,
|
||||
"Failed to fetch currency exchange rate",
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +86,7 @@ impl PaymentIntentMetricAccumulator for CountAccumulator {
|
||||
}
|
||||
|
||||
impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
|
||||
type MetricOutput = (Option<u64>, Option<u64>);
|
||||
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
|
||||
#[inline]
|
||||
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
|
||||
self.amount = match (
|
||||
@ -117,7 +117,7 @@ impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
|
||||
.amount_without_retries
|
||||
.and_then(|i| u64::try_from(i).ok())
|
||||
.or(Some(0));
|
||||
(with_retries, without_retries)
|
||||
(with_retries, without_retries, Some(0), Some(0))
|
||||
}
|
||||
}
|
||||
|
||||
@ -185,7 +185,14 @@ impl PaymentIntentMetricAccumulator for PaymentsSuccessRateAccumulator {
|
||||
}
|
||||
|
||||
impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
|
||||
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
|
||||
type MetricOutput = (
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
);
|
||||
#[inline]
|
||||
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
|
||||
self.total_with_retries = match (
|
||||
@ -235,6 +242,8 @@ impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
|
||||
count_with_retries,
|
||||
total_without_retries,
|
||||
count_without_retries,
|
||||
Some(0),
|
||||
Some(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -301,13 +310,19 @@ impl PaymentIntentMetricsAccumulator {
|
||||
payments_success_rate,
|
||||
payments_success_rate_without_smart_retries,
|
||||
) = self.payments_success_rate.collect();
|
||||
let (smart_retried_amount, smart_retried_amount_without_smart_retries) =
|
||||
self.smart_retried_amount.collect();
|
||||
let (
|
||||
smart_retried_amount,
|
||||
smart_retried_amount_without_smart_retries,
|
||||
smart_retried_amount_in_usd,
|
||||
smart_retried_amount_without_smart_retries_in_usd,
|
||||
) = self.smart_retried_amount.collect();
|
||||
let (
|
||||
payment_processed_amount,
|
||||
payment_processed_count,
|
||||
payment_processed_amount_without_smart_retries,
|
||||
payment_processed_count_without_smart_retries,
|
||||
payment_processed_amount_in_usd,
|
||||
payment_processed_amount_without_smart_retries_in_usd,
|
||||
) = self.payment_processed_amount.collect();
|
||||
let (
|
||||
payments_success_rate_distribution_without_smart_retries,
|
||||
@ -317,7 +332,9 @@ impl PaymentIntentMetricsAccumulator {
|
||||
successful_smart_retries: self.successful_smart_retries.collect(),
|
||||
total_smart_retries: self.total_smart_retries.collect(),
|
||||
smart_retried_amount,
|
||||
smart_retried_amount_in_usd,
|
||||
smart_retried_amount_without_smart_retries,
|
||||
smart_retried_amount_without_smart_retries_in_usd,
|
||||
payment_intent_count: self.payment_intent_count.collect(),
|
||||
successful_payments,
|
||||
successful_payments_without_smart_retries,
|
||||
@ -330,6 +347,8 @@ impl PaymentIntentMetricsAccumulator {
|
||||
payment_processed_count_without_smart_retries,
|
||||
payments_success_rate_distribution_without_smart_retries,
|
||||
payments_failure_rate_distribution_without_smart_retries,
|
||||
payment_processed_amount_in_usd,
|
||||
payment_processed_amount_without_smart_retries_in_usd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,8 +10,10 @@ use api_models::analytics::{
|
||||
PaymentIntentFiltersResponse, PaymentIntentsAnalyticsMetadata, PaymentIntentsMetricsResponse,
|
||||
SankeyResponse,
|
||||
};
|
||||
use common_enums::IntentStatus;
|
||||
use bigdecimal::ToPrimitive;
|
||||
use common_enums::{Currency, IntentStatus};
|
||||
use common_utils::{errors::CustomResult, types::TimeRange};
|
||||
use currency_conversion::{conversion::convert, types::ExchangeRates};
|
||||
use error_stack::ResultExt;
|
||||
use router_env::{
|
||||
instrument, logger,
|
||||
@ -120,6 +122,7 @@ pub async fn get_sankey(
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_metrics(
|
||||
pool: &AnalyticsProvider,
|
||||
ex_rates: &ExchangeRates,
|
||||
auth: &AuthInfo,
|
||||
req: GetPaymentIntentMetricRequest,
|
||||
) -> AnalyticsResult<PaymentIntentsMetricsResponse<MetricsBucketResponse>> {
|
||||
@ -227,16 +230,20 @@ pub async fn get_metrics(
|
||||
let mut success = 0;
|
||||
let mut success_without_smart_retries = 0;
|
||||
let mut total_smart_retried_amount = 0;
|
||||
let mut total_smart_retried_amount_in_usd = 0;
|
||||
let mut total_smart_retried_amount_without_smart_retries = 0;
|
||||
let mut total_smart_retried_amount_without_smart_retries_in_usd = 0;
|
||||
let mut total = 0;
|
||||
let mut total_payment_processed_amount = 0;
|
||||
let mut total_payment_processed_amount_in_usd = 0;
|
||||
let mut total_payment_processed_count = 0;
|
||||
let mut total_payment_processed_amount_without_smart_retries = 0;
|
||||
let mut total_payment_processed_amount_without_smart_retries_in_usd = 0;
|
||||
let mut total_payment_processed_count_without_smart_retries = 0;
|
||||
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
|
||||
.into_iter()
|
||||
.map(|(id, val)| {
|
||||
let collected_values = val.collect();
|
||||
let mut collected_values = val.collect();
|
||||
if let Some(success_count) = collected_values.successful_payments {
|
||||
success += success_count;
|
||||
}
|
||||
@ -248,20 +255,95 @@ pub async fn get_metrics(
|
||||
total += total_count;
|
||||
}
|
||||
if let Some(retried_amount) = collected_values.smart_retried_amount {
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(retried_amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.smart_retried_amount_in_usd = amount_in_usd;
|
||||
total_smart_retried_amount += retried_amount;
|
||||
total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0);
|
||||
}
|
||||
if let Some(retried_amount) =
|
||||
collected_values.smart_retried_amount_without_smart_retries
|
||||
{
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(retried_amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd;
|
||||
total_smart_retried_amount_without_smart_retries += retried_amount;
|
||||
total_smart_retried_amount_without_smart_retries_in_usd +=
|
||||
amount_in_usd.unwrap_or(0);
|
||||
}
|
||||
if let Some(amount) = collected_values.payment_processed_amount {
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.payment_processed_amount_in_usd = amount_in_usd;
|
||||
total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0);
|
||||
total_payment_processed_amount += amount;
|
||||
}
|
||||
if let Some(count) = collected_values.payment_processed_count {
|
||||
total_payment_processed_count += count;
|
||||
}
|
||||
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.payment_processed_amount_without_smart_retries_in_usd =
|
||||
amount_in_usd;
|
||||
total_payment_processed_amount_without_smart_retries_in_usd +=
|
||||
amount_in_usd.unwrap_or(0);
|
||||
total_payment_processed_amount_without_smart_retries += amount;
|
||||
}
|
||||
if let Some(count) = collected_values.payment_processed_count_without_smart_retries {
|
||||
@ -294,6 +376,14 @@ pub async fn get_metrics(
|
||||
total_payment_processed_amount_without_smart_retries: Some(
|
||||
total_payment_processed_amount_without_smart_retries,
|
||||
),
|
||||
total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd),
|
||||
total_smart_retried_amount_without_smart_retries_in_usd: Some(
|
||||
total_smart_retried_amount_without_smart_retries_in_usd,
|
||||
),
|
||||
total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd),
|
||||
total_payment_processed_amount_without_smart_retries_in_usd: Some(
|
||||
total_payment_processed_amount_without_smart_retries_in_usd,
|
||||
),
|
||||
total_payment_processed_count: Some(total_payment_processed_count),
|
||||
total_payment_processed_count_without_smart_retries: Some(
|
||||
total_payment_processed_count_without_smart_retries,
|
||||
|
||||
@ -61,7 +61,7 @@ where
|
||||
query_builder
|
||||
.add_select_column("attempt_count == 1 as first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder.add_select_column("currency").switch()?;
|
||||
query_builder
|
||||
.add_select_column(Aggregate::Sum {
|
||||
field: "amount",
|
||||
@ -101,7 +101,10 @@ where
|
||||
.add_group_by_clause("attempt_count")
|
||||
.attach_printable("Error grouping by attempt_count")
|
||||
.switch()?;
|
||||
|
||||
query_builder
|
||||
.add_group_by_clause("currency")
|
||||
.attach_printable("Error grouping by currency")
|
||||
.switch()?;
|
||||
if let Some(granularity) = granularity.as_ref() {
|
||||
granularity
|
||||
.set_group_by_clause(&mut query_builder)
|
||||
|
||||
@ -63,6 +63,7 @@ where
|
||||
.add_select_column("attempt_count == 1 as first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder.add_select_column("currency").switch()?;
|
||||
query_builder
|
||||
.add_select_column(Aggregate::Min {
|
||||
field: "created_at",
|
||||
@ -102,7 +103,10 @@ where
|
||||
.add_group_by_clause("first_attempt")
|
||||
.attach_printable("Error grouping by first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder
|
||||
.add_group_by_clause("currency")
|
||||
.attach_printable("Error grouping by first_attempt")
|
||||
.switch()?;
|
||||
if let Some(granularity) = granularity.as_ref() {
|
||||
granularity
|
||||
.set_group_by_clause(&mut query_builder)
|
||||
|
||||
@ -62,7 +62,7 @@ where
|
||||
query_builder
|
||||
.add_select_column("attempt_count == 1 as first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder.add_select_column("currency").switch()?;
|
||||
query_builder
|
||||
.add_select_column(Aggregate::Min {
|
||||
field: "created_at",
|
||||
@ -102,7 +102,10 @@ where
|
||||
.add_group_by_clause("first_attempt")
|
||||
.attach_printable("Error grouping by first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder
|
||||
.add_group_by_clause("currency")
|
||||
.attach_printable("Error grouping by currency")
|
||||
.switch()?;
|
||||
if let Some(granularity) = granularity.as_ref() {
|
||||
granularity
|
||||
.set_group_by_clause(&mut query_builder)
|
||||
|
||||
@ -272,7 +272,14 @@ impl PaymentMetricAccumulator for CountAccumulator {
|
||||
}
|
||||
|
||||
impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
|
||||
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
|
||||
type MetricOutput = (
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
Option<u64>,
|
||||
);
|
||||
#[inline]
|
||||
fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) {
|
||||
self.total_with_retries = match (
|
||||
@ -322,6 +329,8 @@ impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
|
||||
count_with_retries,
|
||||
total_without_retries,
|
||||
count_without_retries,
|
||||
Some(0),
|
||||
Some(0),
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -378,6 +387,8 @@ impl PaymentMetricsAccumulator {
|
||||
payment_processed_count,
|
||||
payment_processed_amount_without_smart_retries,
|
||||
payment_processed_count_without_smart_retries,
|
||||
payment_processed_amount_usd,
|
||||
payment_processed_amount_without_smart_retries_usd,
|
||||
) = self.processed_amount.collect();
|
||||
let (
|
||||
payments_success_rate_distribution,
|
||||
@ -406,6 +417,8 @@ impl PaymentMetricsAccumulator {
|
||||
payments_failure_rate_distribution_without_smart_retries,
|
||||
failure_reason_count,
|
||||
failure_reason_count_without_smart_retries,
|
||||
payment_processed_amount_usd,
|
||||
payment_processed_amount_without_smart_retries_usd,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,10 @@ use api_models::analytics::{
|
||||
FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, PaymentFiltersResponse,
|
||||
PaymentsAnalyticsMetadata, PaymentsMetricsResponse,
|
||||
};
|
||||
use bigdecimal::ToPrimitive;
|
||||
use common_enums::Currency;
|
||||
use common_utils::errors::CustomResult;
|
||||
use currency_conversion::{conversion::convert, types::ExchangeRates};
|
||||
use error_stack::ResultExt;
|
||||
use router_env::{
|
||||
instrument, logger,
|
||||
@ -46,6 +49,7 @@ pub enum TaskType {
|
||||
#[instrument(skip_all)]
|
||||
pub async fn get_metrics(
|
||||
pool: &AnalyticsProvider,
|
||||
ex_rates: &ExchangeRates,
|
||||
auth: &AuthInfo,
|
||||
req: GetPaymentMetricRequest,
|
||||
) -> AnalyticsResult<PaymentsMetricsResponse<MetricsBucketResponse>> {
|
||||
@ -224,18 +228,57 @@ pub async fn get_metrics(
|
||||
let mut total_payment_processed_count_without_smart_retries = 0;
|
||||
let mut total_failure_reasons_count = 0;
|
||||
let mut total_failure_reasons_count_without_smart_retries = 0;
|
||||
let mut total_payment_processed_amount_usd = 0;
|
||||
let mut total_payment_processed_amount_without_smart_retries_usd = 0;
|
||||
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
|
||||
.into_iter()
|
||||
.map(|(id, val)| {
|
||||
let collected_values = val.collect();
|
||||
let mut collected_values = val.collect();
|
||||
if let Some(amount) = collected_values.payment_processed_amount {
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.payment_processed_amount_usd = amount_in_usd;
|
||||
total_payment_processed_amount += amount;
|
||||
total_payment_processed_amount_usd += amount_in_usd.unwrap_or(0);
|
||||
}
|
||||
if let Some(count) = collected_values.payment_processed_count {
|
||||
total_payment_processed_count += count;
|
||||
}
|
||||
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
|
||||
let amount_in_usd = id
|
||||
.currency
|
||||
.and_then(|currency| {
|
||||
i64::try_from(amount)
|
||||
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
|
||||
.ok()
|
||||
.and_then(|amount_i64| {
|
||||
convert(ex_rates, currency, Currency::USD, amount_i64)
|
||||
.inspect_err(|e| {
|
||||
logger::error!("Currency conversion error: {:?}", e)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
})
|
||||
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
|
||||
.unwrap_or_default();
|
||||
collected_values.payment_processed_amount_without_smart_retries_usd = amount_in_usd;
|
||||
total_payment_processed_amount_without_smart_retries += amount;
|
||||
total_payment_processed_amount_without_smart_retries_usd +=
|
||||
amount_in_usd.unwrap_or(0);
|
||||
}
|
||||
if let Some(count) = collected_values.payment_processed_count_without_smart_retries {
|
||||
total_payment_processed_count_without_smart_retries += count;
|
||||
@ -252,14 +295,17 @@ pub async fn get_metrics(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(PaymentsMetricsResponse {
|
||||
query_data,
|
||||
meta_data: [PaymentsAnalyticsMetadata {
|
||||
total_payment_processed_amount: Some(total_payment_processed_amount),
|
||||
total_payment_processed_amount_usd: Some(total_payment_processed_amount_usd),
|
||||
total_payment_processed_amount_without_smart_retries: Some(
|
||||
total_payment_processed_amount_without_smart_retries,
|
||||
),
|
||||
total_payment_processed_amount_without_smart_retries_usd: Some(
|
||||
total_payment_processed_amount_without_smart_retries_usd,
|
||||
),
|
||||
total_payment_processed_count: Some(total_payment_processed_count),
|
||||
total_payment_processed_count_without_smart_retries: Some(
|
||||
total_payment_processed_count_without_smart_retries,
|
||||
|
||||
@ -50,6 +50,7 @@ where
|
||||
alias: Some("total"),
|
||||
})
|
||||
.switch()?;
|
||||
query_builder.add_select_column("currency").switch()?;
|
||||
query_builder
|
||||
.add_select_column(Aggregate::Min {
|
||||
field: "created_at",
|
||||
@ -79,6 +80,11 @@ where
|
||||
.switch()?;
|
||||
}
|
||||
|
||||
query_builder
|
||||
.add_group_by_clause("currency")
|
||||
.attach_printable("Error grouping by currency")
|
||||
.switch()?;
|
||||
|
||||
if let Some(granularity) = granularity.as_ref() {
|
||||
granularity
|
||||
.set_group_by_clause(&mut query_builder)
|
||||
|
||||
@ -57,6 +57,8 @@ where
|
||||
|
||||
query_builder.add_select_column("first_attempt").switch()?;
|
||||
|
||||
query_builder.add_select_column("currency").switch()?;
|
||||
|
||||
query_builder
|
||||
.add_select_column(Aggregate::Sum {
|
||||
field: "amount",
|
||||
@ -95,6 +97,12 @@ where
|
||||
.add_group_by_clause("first_attempt")
|
||||
.attach_printable("Error grouping by first_attempt")
|
||||
.switch()?;
|
||||
|
||||
query_builder
|
||||
.add_group_by_clause("currency")
|
||||
.attach_printable("Error grouping by currency")
|
||||
.switch()?;
|
||||
|
||||
if let Some(granularity) = granularity.as_ref() {
|
||||
granularity
|
||||
.set_group_by_clause(&mut query_builder)
|
||||
|
||||
@ -203,7 +203,9 @@ pub struct AnalyticsMetadata {
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct PaymentsAnalyticsMetadata {
|
||||
pub total_payment_processed_amount: Option<u64>,
|
||||
pub total_payment_processed_amount_usd: Option<u64>,
|
||||
pub total_payment_processed_amount_without_smart_retries: Option<u64>,
|
||||
pub total_payment_processed_amount_without_smart_retries_usd: Option<u64>,
|
||||
pub total_payment_processed_count: Option<u64>,
|
||||
pub total_payment_processed_count_without_smart_retries: Option<u64>,
|
||||
pub total_failure_reasons_count: Option<u64>,
|
||||
@ -218,6 +220,10 @@ pub struct PaymentIntentsAnalyticsMetadata {
|
||||
pub total_smart_retried_amount_without_smart_retries: Option<u64>,
|
||||
pub total_payment_processed_amount: Option<u64>,
|
||||
pub total_payment_processed_amount_without_smart_retries: Option<u64>,
|
||||
pub total_smart_retried_amount_in_usd: Option<u64>,
|
||||
pub total_smart_retried_amount_without_smart_retries_in_usd: Option<u64>,
|
||||
pub total_payment_processed_amount_in_usd: Option<u64>,
|
||||
pub total_payment_processed_amount_without_smart_retries_in_usd: Option<u64>,
|
||||
pub total_payment_processed_count: Option<u64>,
|
||||
pub total_payment_processed_count_without_smart_retries: Option<u64>,
|
||||
}
|
||||
|
||||
@ -161,7 +161,9 @@ pub struct PaymentIntentMetricsBucketValue {
|
||||
pub successful_smart_retries: Option<u64>,
|
||||
pub total_smart_retries: Option<u64>,
|
||||
pub smart_retried_amount: Option<u64>,
|
||||
pub smart_retried_amount_in_usd: Option<u64>,
|
||||
pub smart_retried_amount_without_smart_retries: Option<u64>,
|
||||
pub smart_retried_amount_without_smart_retries_in_usd: Option<u64>,
|
||||
pub payment_intent_count: Option<u64>,
|
||||
pub successful_payments: Option<u32>,
|
||||
pub successful_payments_without_smart_retries: Option<u32>,
|
||||
@ -169,8 +171,10 @@ pub struct PaymentIntentMetricsBucketValue {
|
||||
pub payments_success_rate: Option<f64>,
|
||||
pub payments_success_rate_without_smart_retries: Option<f64>,
|
||||
pub payment_processed_amount: Option<u64>,
|
||||
pub payment_processed_amount_in_usd: Option<u64>,
|
||||
pub payment_processed_count: Option<u64>,
|
||||
pub payment_processed_amount_without_smart_retries: Option<u64>,
|
||||
pub payment_processed_amount_without_smart_retries_in_usd: Option<u64>,
|
||||
pub payment_processed_count_without_smart_retries: Option<u64>,
|
||||
pub payments_success_rate_distribution_without_smart_retries: Option<f64>,
|
||||
pub payments_failure_rate_distribution_without_smart_retries: Option<f64>,
|
||||
|
||||
@ -271,8 +271,10 @@ pub struct PaymentMetricsBucketValue {
|
||||
pub payment_count: Option<u64>,
|
||||
pub payment_success_count: Option<u64>,
|
||||
pub payment_processed_amount: Option<u64>,
|
||||
pub payment_processed_amount_usd: Option<u64>,
|
||||
pub payment_processed_count: Option<u64>,
|
||||
pub payment_processed_amount_without_smart_retries: Option<u64>,
|
||||
pub payment_processed_amount_without_smart_retries_usd: Option<u64>,
|
||||
pub payment_processed_count_without_smart_retries: Option<u64>,
|
||||
pub avg_ticket_size: Option<f64>,
|
||||
pub payment_error_message: Option<Vec<ErrorResult>>,
|
||||
|
||||
@ -32,7 +32,10 @@ pub mod routes {
|
||||
|
||||
use crate::{
|
||||
consts::opensearch::SEARCH_INDEXES,
|
||||
core::{api_locking, errors::user::UserErrors, verification::utils},
|
||||
core::{
|
||||
api_locking, currency::get_forex_exchange_rates, errors::user::UserErrors,
|
||||
verification::utils,
|
||||
},
|
||||
db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload},
|
||||
routes::AppState,
|
||||
services::{
|
||||
@ -397,7 +400,8 @@ pub mod routes {
|
||||
org_id: org_id.clone(),
|
||||
merchant_ids: vec![merchant_id.clone()],
|
||||
};
|
||||
analytics::payments::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
@ -435,7 +439,8 @@ pub mod routes {
|
||||
let auth: AuthInfo = AuthInfo::OrgLevel {
|
||||
org_id: org_id.clone(),
|
||||
};
|
||||
analytics::payments::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
@ -480,7 +485,8 @@ pub mod routes {
|
||||
merchant_id: merchant_id.clone(),
|
||||
profile_ids: vec![profile_id.clone()],
|
||||
};
|
||||
analytics::payments::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
@ -520,7 +526,8 @@ pub mod routes {
|
||||
org_id: org_id.clone(),
|
||||
merchant_ids: vec![merchant_id.clone()],
|
||||
};
|
||||
analytics::payment_intents::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
@ -558,7 +565,8 @@ pub mod routes {
|
||||
let auth: AuthInfo = AuthInfo::OrgLevel {
|
||||
org_id: org_id.clone(),
|
||||
};
|
||||
analytics::payment_intents::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
@ -603,7 +611,8 @@ pub mod routes {
|
||||
merchant_id: merchant_id.clone(),
|
||||
profile_ids: vec![profile_id.clone()],
|
||||
};
|
||||
analytics::payment_intents::get_metrics(&state.pool, &auth, req)
|
||||
let ex_rates = get_forex_exchange_rates(state.clone()).await?;
|
||||
analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
use analytics::errors::AnalyticsError;
|
||||
use common_utils::errors::CustomResult;
|
||||
use currency_conversion::types::ExchangeRates;
|
||||
use error_stack::ResultExt;
|
||||
|
||||
use crate::{
|
||||
@ -46,3 +48,19 @@ pub async fn convert_forex(
|
||||
.change_context(ApiErrorResponse::InternalServerError)?,
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_forex_exchange_rates(
|
||||
state: SessionState,
|
||||
) -> CustomResult<ExchangeRates, AnalyticsError> {
|
||||
let forex_api = state.conf.forex_api.get_inner();
|
||||
let rates = get_forex_rates(
|
||||
&state,
|
||||
forex_api.call_delay,
|
||||
forex_api.local_fetch_retry_delay,
|
||||
forex_api.local_fetch_retry_count,
|
||||
)
|
||||
.await
|
||||
.change_context(AnalyticsError::ForexFetchFailed)?;
|
||||
|
||||
Ok((*rates.data).clone())
|
||||
}
|
||||
|
||||
@ -26,7 +26,7 @@ const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD";
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
pub struct FxExchangeRatesCacheEntry {
|
||||
data: Arc<ExchangeRates>,
|
||||
pub data: Arc<ExchangeRates>,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
@ -421,7 +421,13 @@ pub async fn fallback_fetch_forex_rates(
|
||||
conversions.insert(enum_curr, currency_factors);
|
||||
}
|
||||
None => {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
if enum_curr == enums::Currency::USD {
|
||||
let currency_factors =
|
||||
CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0));
|
||||
conversions.insert(enum_curr, currency_factors);
|
||||
} else {
|
||||
logger::error!("Rates for {} not received from API", &enum_curr);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user