From df9169b606fb1b6903639d51f609ebcad28938be Mon Sep 17 00:00:00 2001 From: Uzair Khan Date: Wed, 6 Nov 2024 14:46:16 +0530 Subject: [PATCH] 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> --- Cargo.lock | 2 + crates/analytics/Cargo.toml | 2 + crates/analytics/src/errors.rs | 8 ++ .../src/payment_intents/accumulator.rs | 29 +++++- crates/analytics/src/payment_intents/core.rs | 94 ++++++++++++++++++- .../payment_processed_amount.rs | 7 +- .../smart_retried_amount.rs | 6 +- .../metrics/smart_retried_amount.rs | 7 +- crates/analytics/src/payments/accumulator.rs | 15 ++- crates/analytics/src/payments/core.rs | 50 +++++++++- .../metrics/payment_processed_amount.rs | 6 ++ .../payment_processed_amount.rs | 8 ++ crates/api_models/src/analytics.rs | 6 ++ .../src/analytics/payment_intents.rs | 4 + crates/api_models/src/analytics/payments.rs | 2 + crates/router/src/analytics.rs | 23 +++-- crates/router/src/core/currency.rs | 18 ++++ crates/router/src/utils/currency.rs | 10 +- 18 files changed, 273 insertions(+), 24 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 235c64352faa..86dd1e4b790b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", diff --git a/crates/analytics/Cargo.toml b/crates/analytics/Cargo.toml index 6cf886896c3c..34732c55e4e7 100644 --- a/crates/analytics/Cargo.toml +++ b/crates/analytics/Cargo.toml @@ -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"] } diff --git a/crates/analytics/src/errors.rs b/crates/analytics/src/errors.rs index 0e39a4ddd928..d7b15a6db115 100644 --- a/crates/analytics/src/errors.rs +++ b/crates/analytics/src/errors.rs @@ -12,6 +12,8 @@ pub enum AnalyticsError { UnknownError, #[error("Access Forbidden Analytics Error")] AccessForbiddenError, + #[error("Failed to fetch currency exchange rate")] + ForexFetchFailed, } impl ErrorSwitch for AnalyticsError { @@ -32,6 +34,12 @@ impl ErrorSwitch 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, + )), } } } diff --git a/crates/analytics/src/payment_intents/accumulator.rs b/crates/analytics/src/payment_intents/accumulator.rs index cbb8335cea01..ef3cd3129c48 100644 --- a/crates/analytics/src/payment_intents/accumulator.rs +++ b/crates/analytics/src/payment_intents/accumulator.rs @@ -86,7 +86,7 @@ impl PaymentIntentMetricAccumulator for CountAccumulator { } impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator { - type MetricOutput = (Option, Option); + type MetricOutput = (Option, Option, Option, Option); #[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, Option, Option, Option); + type MetricOutput = ( + Option, + Option, + Option, + Option, + Option, + Option, + ); #[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, } } } diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 3e8915c60a2a..7ea8e9007f7b 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -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> { @@ -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 = 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, diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs index e77722450630..01d580534834 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs @@ -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) diff --git a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs index 6d36aca5172b..cf7af6e11e7e 100644 --- a/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs @@ -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) diff --git a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs index b23fcafdee08..9497dc89f42c 100644 --- a/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs +++ b/crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs @@ -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) diff --git a/crates/analytics/src/payments/accumulator.rs b/crates/analytics/src/payments/accumulator.rs index 4388b2071fee..651eeb0bcfe7 100644 --- a/crates/analytics/src/payments/accumulator.rs +++ b/crates/analytics/src/payments/accumulator.rs @@ -272,7 +272,14 @@ impl PaymentMetricAccumulator for CountAccumulator { } impl PaymentMetricAccumulator for ProcessedAmountAccumulator { - type MetricOutput = (Option, Option, Option, Option); + type MetricOutput = ( + Option, + Option, + Option, + Option, + Option, + Option, + ); #[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, } } } diff --git a/crates/analytics/src/payments/core.rs b/crates/analytics/src/payments/core.rs index 59ae549b2839..bcd009270dc1 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -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> { @@ -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 = 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, diff --git a/crates/analytics/src/payments/metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/payment_processed_amount.rs index b8b3868803c6..fa54c1730416 100644 --- a/crates/analytics/src/payments/metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/payment_processed_amount.rs @@ -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) diff --git a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs index 9bc554eaae71..a315b2fc4c82 100644 --- a/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs +++ b/crates/analytics/src/payments/metrics/sessionized_metrics/payment_processed_amount.rs @@ -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) diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index b95404080b03..8d63bc3096ca 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -203,7 +203,9 @@ pub struct AnalyticsMetadata { #[derive(Debug, serde::Serialize)] pub struct PaymentsAnalyticsMetadata { pub total_payment_processed_amount: Option, + pub total_payment_processed_amount_usd: Option, pub total_payment_processed_amount_without_smart_retries: Option, + pub total_payment_processed_amount_without_smart_retries_usd: Option, pub total_payment_processed_count: Option, pub total_payment_processed_count_without_smart_retries: Option, pub total_failure_reasons_count: Option, @@ -218,6 +220,10 @@ pub struct PaymentIntentsAnalyticsMetadata { pub total_smart_retried_amount_without_smart_retries: Option, pub total_payment_processed_amount: Option, pub total_payment_processed_amount_without_smart_retries: Option, + pub total_smart_retried_amount_in_usd: Option, + pub total_smart_retried_amount_without_smart_retries_in_usd: Option, + pub total_payment_processed_amount_in_usd: Option, + pub total_payment_processed_amount_without_smart_retries_in_usd: Option, pub total_payment_processed_count: Option, pub total_payment_processed_count_without_smart_retries: Option, } diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 60662f2e90af..3ac3c09d35f6 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -161,7 +161,9 @@ pub struct PaymentIntentMetricsBucketValue { pub successful_smart_retries: Option, pub total_smart_retries: Option, pub smart_retried_amount: Option, + pub smart_retried_amount_in_usd: Option, pub smart_retried_amount_without_smart_retries: Option, + pub smart_retried_amount_without_smart_retries_in_usd: Option, pub payment_intent_count: Option, pub successful_payments: Option, pub successful_payments_without_smart_retries: Option, @@ -169,8 +171,10 @@ pub struct PaymentIntentMetricsBucketValue { pub payments_success_rate: Option, pub payments_success_rate_without_smart_retries: Option, pub payment_processed_amount: Option, + pub payment_processed_amount_in_usd: Option, pub payment_processed_count: Option, pub payment_processed_amount_without_smart_retries: Option, + pub payment_processed_amount_without_smart_retries_in_usd: Option, pub payment_processed_count_without_smart_retries: Option, pub payments_success_rate_distribution_without_smart_retries: Option, pub payments_failure_rate_distribution_without_smart_retries: Option, diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 1120ab092d75..1faba79eb378 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -271,8 +271,10 @@ pub struct PaymentMetricsBucketValue { pub payment_count: Option, pub payment_success_count: Option, pub payment_processed_amount: Option, + pub payment_processed_amount_usd: Option, pub payment_processed_count: Option, pub payment_processed_amount_without_smart_retries: Option, + pub payment_processed_amount_without_smart_retries_usd: Option, pub payment_processed_count_without_smart_retries: Option, pub avg_ticket_size: Option, pub payment_error_message: Option>, diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index 150931e9c8a7..aba1c2e2efaa 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -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) }, diff --git a/crates/router/src/core/currency.rs b/crates/router/src/core/currency.rs index 96d75098271b..912484b014a7 100644 --- a/crates/router/src/core/currency.rs +++ b/crates/router/src/core/currency.rs @@ -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 { + 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()) +} diff --git a/crates/router/src/utils/currency.rs b/crates/router/src/utils/currency.rs index dcfe0347d6fa..2173478ab673 100644 --- a/crates/router/src/utils/currency.rs +++ b/crates/router/src/utils/currency.rs @@ -26,7 +26,7 @@ const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD"; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct FxExchangeRatesCacheEntry { - data: Arc, + pub data: Arc, 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); + } } }; }