diff --git a/config/config.example.toml b/config/config.example.toml index 0fe6b8caa2..5c55d57cf7 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -661,6 +661,7 @@ pm_auth_key = "Some_pm_auth_key" # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used +forex_enabled = false # Enable or disable forex conversion for analytics [analytics.clickhouse] username = "" # Clickhouse username diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 848a2305ae..02d3fe66c0 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -9,6 +9,7 @@ database_name = "clickhouse_db_name" # Clickhouse database name # Analytics configuration. [analytics] source = "sqlx" # The Analytics source/strategy to be used +forex_enabled = false # Boolean to enable or disable forex conversion [analytics.sqlx] username = "db_user" # Analytics DB Username diff --git a/config/development.toml b/config/development.toml index 077e09f046..fd84d626d3 100644 --- a/config/development.toml +++ b/config/development.toml @@ -719,6 +719,7 @@ authentication_analytics_topic = "hyperswitch-authentication-events" [analytics] source = "sqlx" +forex_enabled = false [analytics.clickhouse] username = "default" diff --git a/crates/analytics/docs/README.md b/crates/analytics/docs/README.md index eb5c26a6ba..e24dc6c5af 100644 --- a/crates/analytics/docs/README.md +++ b/crates/analytics/docs/README.md @@ -104,6 +104,12 @@ To use Forex services, you need to sign up and get your API keys from the follow - It will be in dashboard, labeled as `access key`. ### Configuring Forex APIs +To enable Forex functionality, update the `config/development.toml` or `config/docker_compose.toml` file: + +```toml +[analytics] +forex_enabled = true # default set to false +``` To configure the Forex APIs, update the `config/development.toml` or `config/docker_compose.toml` file with your API keys: diff --git a/crates/analytics/src/lib.rs b/crates/analytics/src/lib.rs index 6d694caadf..0ad8886b82 100644 --- a/crates/analytics/src/lib.rs +++ b/crates/analytics/src/lib.rs @@ -969,21 +969,25 @@ impl AnalyticsProvider { tenant: &dyn storage_impl::config::TenantConfig, ) -> Self { match config { - AnalyticsConfig::Sqlx { sqlx } => { + AnalyticsConfig::Sqlx { sqlx, .. } => { Self::Sqlx(SqlxClient::from_conf(sqlx, tenant.get_schema()).await) } - AnalyticsConfig::Clickhouse { clickhouse } => Self::Clickhouse(ClickhouseClient { + AnalyticsConfig::Clickhouse { clickhouse, .. } => Self::Clickhouse(ClickhouseClient { config: Arc::new(clickhouse.clone()), database: tenant.get_clickhouse_database().to_string(), }), - AnalyticsConfig::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh( + AnalyticsConfig::CombinedCkh { + sqlx, clickhouse, .. + } => Self::CombinedCkh( SqlxClient::from_conf(sqlx, tenant.get_schema()).await, ClickhouseClient { config: Arc::new(clickhouse.clone()), database: tenant.get_clickhouse_database().to_string(), }, ), - AnalyticsConfig::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx( + AnalyticsConfig::CombinedSqlx { + sqlx, clickhouse, .. + } => Self::CombinedSqlx( SqlxClient::from_conf(sqlx, tenant.get_schema()).await, ClickhouseClient { config: Arc::new(clickhouse.clone()), @@ -1000,20 +1004,35 @@ impl AnalyticsProvider { pub enum AnalyticsConfig { Sqlx { sqlx: Database, + forex_enabled: bool, }, Clickhouse { clickhouse: ClickhouseConfig, + forex_enabled: bool, }, CombinedCkh { sqlx: Database, clickhouse: ClickhouseConfig, + forex_enabled: bool, }, CombinedSqlx { sqlx: Database, clickhouse: ClickhouseConfig, + forex_enabled: bool, }, } +impl AnalyticsConfig { + pub fn get_forex_enabled(&self) -> bool { + match self { + Self::Sqlx { forex_enabled, .. } + | Self::Clickhouse { forex_enabled, .. } + | Self::CombinedCkh { forex_enabled, .. } + | Self::CombinedSqlx { forex_enabled, .. } => *forex_enabled, + } + } +} + #[async_trait::async_trait] impl SecretsHandler for AnalyticsConfig { async fn convert_to_raw_secret( @@ -1024,7 +1043,7 @@ impl SecretsHandler for AnalyticsConfig { let decrypted_password = match analytics_config { // Todo: Perform kms decryption of clickhouse password Self::Clickhouse { .. } => masking::Secret::new(String::default()), - Self::Sqlx { sqlx } + Self::Sqlx { sqlx, .. } | Self::CombinedCkh { sqlx, .. } | Self::CombinedSqlx { sqlx, .. } => { secret_management_client @@ -1034,26 +1053,46 @@ impl SecretsHandler for AnalyticsConfig { }; Ok(value.transition_state(|conf| match conf { - Self::Sqlx { sqlx } => Self::Sqlx { + Self::Sqlx { + sqlx, + forex_enabled, + } => Self::Sqlx { sqlx: Database { password: decrypted_password, ..sqlx }, + forex_enabled, }, - Self::Clickhouse { clickhouse } => Self::Clickhouse { clickhouse }, - Self::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh { + Self::Clickhouse { + clickhouse, + forex_enabled, + } => Self::Clickhouse { + clickhouse, + forex_enabled, + }, + Self::CombinedCkh { + sqlx, + clickhouse, + forex_enabled, + } => Self::CombinedCkh { sqlx: Database { password: decrypted_password, ..sqlx }, clickhouse, + forex_enabled, }, - Self::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx { + Self::CombinedSqlx { + sqlx, + clickhouse, + forex_enabled, + } => Self::CombinedSqlx { sqlx: Database { password: decrypted_password, ..sqlx }, clickhouse, + forex_enabled, }, })) } @@ -1063,6 +1102,7 @@ impl Default for AnalyticsConfig { fn default() -> Self { Self::Sqlx { sqlx: Database::default(), + forex_enabled: false, } } } diff --git a/crates/analytics/src/payment_intents/core.rs b/crates/analytics/src/payment_intents/core.rs index 00a59d8287..0a512c419e 100644 --- a/crates/analytics/src/payment_intents/core.rs +++ b/crates/analytics/src/payment_intents/core.rs @@ -68,7 +68,7 @@ pub async fn get_sankey( #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetPaymentIntentMetricRequest, ) -> AnalyticsResult> { @@ -201,22 +201,25 @@ 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; 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); @@ -224,44 +227,50 @@ pub async fn get_metrics( 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; 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; @@ -270,22 +279,25 @@ pub async fn get_metrics( 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; collected_values.payment_processed_amount_without_smart_retries_in_usd = amount_in_usd; total_payment_processed_amount_without_smart_retries_in_usd += @@ -322,14 +334,26 @@ 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_smart_retried_amount_in_usd: if ex_rates.is_some() { + Some(total_smart_retried_amount_in_usd) + } else { + None + }, + total_smart_retried_amount_without_smart_retries_in_usd: if ex_rates.is_some() { + Some(total_smart_retried_amount_without_smart_retries_in_usd) + } else { + None + }, + total_payment_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_in_usd) + } else { + None + }, + total_payment_processed_amount_without_smart_retries_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_without_smart_retries_in_usd) + } else { + None + }, 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/core.rs b/crates/analytics/src/payments/core.rs index 86192265d0..7291d2f1fc 100644 --- a/crates/analytics/src/payments/core.rs +++ b/crates/analytics/src/payments/core.rs @@ -48,7 +48,7 @@ pub enum TaskType { #[instrument(skip_all)] pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetPaymentMetricRequest, ) -> AnalyticsResult> { @@ -234,22 +234,25 @@ pub async fn get_metrics( .map(|(id, val)| { 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; collected_values.payment_processed_amount_in_usd = amount_in_usd; total_payment_processed_amount += amount; total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); @@ -258,22 +261,25 @@ pub async fn get_metrics( 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(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; 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 += @@ -298,13 +304,19 @@ pub async fn get_metrics( query_data, meta_data: [PaymentsAnalyticsMetadata { total_payment_processed_amount: Some(total_payment_processed_amount), - total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), + total_payment_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_in_usd) + } else { + None + }, 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_amount_without_smart_retries_usd: if ex_rates.is_some() { + Some(total_payment_processed_amount_without_smart_retries_usd) + } else { + None + }, 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/refunds/core.rs b/crates/analytics/src/refunds/core.rs index 04badd06bd..15ae14206c 100644 --- a/crates/analytics/src/refunds/core.rs +++ b/crates/analytics/src/refunds/core.rs @@ -47,7 +47,7 @@ pub enum TaskType { pub async fn get_metrics( pool: &AnalyticsProvider, - ex_rates: &ExchangeRates, + ex_rates: &Option, auth: &AuthInfo, req: GetRefundMetricRequest, ) -> AnalyticsResult> { @@ -217,22 +217,25 @@ pub async fn get_metrics( total += total_count; } if let Some(amount) = collected_values.refund_processed_amount { - let amount_in_usd = id - .currency - .and_then(|currency| { - i64::try_from(amount) - .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) - .ok() - .and_then(|amount_i64| { - convert(ex_rates, currency, Currency::USD, amount_i64) - .inspect_err(|e| { - logger::error!("Currency conversion error: {:?}", e) - }) - .ok() - }) - }) - .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) - .unwrap_or_default(); + let amount_in_usd = if let Some(ex_rates) = ex_rates { + 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() + } else { + None + }; collected_values.refund_processed_amount_in_usd = amount_in_usd; total_refund_processed_amount += amount; total_refund_processed_amount_in_usd += amount_in_usd.unwrap_or(0); @@ -261,7 +264,11 @@ pub async fn get_metrics( meta_data: [RefundsAnalyticsMetadata { total_refund_success_rate, total_refund_processed_amount: Some(total_refund_processed_amount), - total_refund_processed_amount_in_usd: Some(total_refund_processed_amount_in_usd), + total_refund_processed_amount_in_usd: if ex_rates.is_some() { + Some(total_refund_processed_amount_in_usd) + } else { + None + }, total_refund_processed_count: Some(total_refund_processed_count), total_refund_reason_count: Some(total_refund_reason_count), total_refund_error_message_count: Some(total_refund_error_message_count), diff --git a/crates/api_models/src/analytics.rs b/crates/api_models/src/analytics.rs index 7f58c6b00d..132272f0e4 100644 --- a/crates/api_models/src/analytics.rs +++ b/crates/api_models/src/analytics.rs @@ -62,7 +62,42 @@ pub enum Granularity { #[serde(rename = "G_ONEDAY")] OneDay, } +pub trait ForexMetric { + fn is_forex_metric(&self) -> bool; +} +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AnalyticsRequest { + pub payment_intent: Option, + pub payment_attempt: Option, + pub refund: Option, + pub dispute: Option, +} + +impl AnalyticsRequest { + pub fn requires_forex_functionality(&self) -> bool { + self.payment_attempt + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .payment_intent + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .refund + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + || self + .dispute + .as_ref() + .map(|req| req.metrics.iter().any(|metric| metric.is_forex_metric())) + .unwrap_or_default() + } +} #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] #[serde(rename_all = "camelCase")] pub struct GetPaymentMetricRequest { diff --git a/crates/api_models/src/analytics/disputes.rs b/crates/api_models/src/analytics/disputes.rs index 2509d83e13..e373704b87 100644 --- a/crates/api_models/src/analytics/disputes.rs +++ b/crates/api_models/src/analytics/disputes.rs @@ -3,7 +3,7 @@ use std::{ hash::{Hash, Hasher}, }; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::DisputeStage; #[derive( @@ -28,6 +28,14 @@ pub enum DisputeMetrics { SessionizedTotalAmountDisputed, SessionizedTotalDisputeLostAmount, } +impl ForexMetric for DisputeMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::TotalAmountDisputed | Self::TotalDisputeLostAmount + ) + } +} #[derive( Debug, diff --git a/crates/api_models/src/analytics/payment_intents.rs b/crates/api_models/src/analytics/payment_intents.rs index 365abd71ed..15c107dfb6 100644 --- a/crates/api_models/src/analytics/payment_intents.rs +++ b/crates/api_models/src/analytics/payment_intents.rs @@ -5,7 +5,7 @@ use std::{ use common_utils::id_type; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::{ AuthenticationType, Connector, Currency, IntentStatus, PaymentMethod, PaymentMethodType, }; @@ -106,6 +106,17 @@ pub enum PaymentIntentMetrics { SessionizedPaymentProcessedAmount, SessionizedPaymentsDistribution, } +impl ForexMetric for PaymentIntentMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::PaymentProcessedAmount + | Self::SmartRetriedAmount + | Self::SessionizedPaymentProcessedAmount + | Self::SessionizedSmartRetriedAmount + ) + } +} #[derive(Debug, Default, serde::Serialize)] pub struct ErrorResult { diff --git a/crates/api_models/src/analytics/payments.rs b/crates/api_models/src/analytics/payments.rs index 8fd24f1512..691e827043 100644 --- a/crates/api_models/src/analytics/payments.rs +++ b/crates/api_models/src/analytics/payments.rs @@ -5,7 +5,7 @@ use std::{ use common_utils::id_type; -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; use crate::enums::{ AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod, PaymentMethodType, @@ -119,6 +119,17 @@ pub enum PaymentMetrics { FailureReasons, } +impl ForexMetric for PaymentMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::PaymentProcessedAmount + | Self::AvgTicketSize + | Self::SessionizedPaymentProcessedAmount + | Self::SessionizedAvgTicketSize + ) + } +} #[derive(Debug, Default, serde::Serialize)] pub struct ErrorResult { pub reason: String, diff --git a/crates/api_models/src/analytics/refunds.rs b/crates/api_models/src/analytics/refunds.rs index 0afca6c1ef..84954e7091 100644 --- a/crates/api_models/src/analytics/refunds.rs +++ b/crates/api_models/src/analytics/refunds.rs @@ -30,7 +30,7 @@ pub enum RefundType { RetryRefund, } -use super::{NameDescription, TimeRange}; +use super::{ForexMetric, NameDescription, TimeRange}; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] pub struct RefundFilters { #[serde(default)] @@ -137,6 +137,14 @@ pub enum RefundDistributions { #[strum(serialize = "refund_error_message")] SessionizedRefundErrorMessage, } +impl ForexMetric for RefundMetrics { + fn is_forex_metric(&self) -> bool { + matches!( + self, + Self::RefundProcessedAmount | Self::SessionizedRefundProcessedAmount + ) + } +} pub mod metric_behaviour { pub struct RefundSuccessRate; diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index ff2744b824..8fe30aa59d 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -18,12 +18,12 @@ pub mod routes { search::{ GetGlobalSearchRequest, GetSearchRequest, GetSearchRequestWithIndex, SearchIndex, }, - GenerateReportRequest, GetActivePaymentsMetricRequest, GetApiEventFiltersRequest, - GetApiEventMetricRequest, GetAuthEventMetricRequest, GetDisputeMetricRequest, - GetFrmFilterRequest, GetFrmMetricRequest, GetPaymentFiltersRequest, - GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, GetPaymentMetricRequest, - GetRefundFilterRequest, GetRefundMetricRequest, GetSdkEventFiltersRequest, - GetSdkEventMetricRequest, ReportRequest, + AnalyticsRequest, GenerateReportRequest, GetActivePaymentsMetricRequest, + GetApiEventFiltersRequest, GetApiEventMetricRequest, GetAuthEventMetricRequest, + GetDisputeMetricRequest, GetFrmFilterRequest, GetFrmMetricRequest, + GetPaymentFiltersRequest, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, + GetPaymentMetricRequest, GetRefundFilterRequest, GetRefundMetricRequest, + GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest, }; use common_enums::EntityType; use common_utils::types::TimeRange; @@ -31,11 +31,9 @@ pub mod routes { use futures::{stream::FuturesUnordered, StreamExt}; use crate::{ + analytics_validator::request_validator, consts::opensearch::SEARCH_INDEXES, - core::{ - api_locking, currency::get_forex_exchange_rates, errors::user::UserErrors, - verification::utils, - }, + core::{api_locking, errors::user::UserErrors, verification::utils}, db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload}, routes::AppState, services::{ @@ -405,7 +403,15 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -444,7 +450,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -491,7 +506,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_attempt: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -532,7 +556,16 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -571,7 +604,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -618,7 +660,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + payment_intent: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -659,7 +710,16 @@ pub mod routes { org_id: org_id.clone(), merchant_ids: vec![merchant_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -698,7 +758,16 @@ pub mod routes { let auth: AuthInfo = AuthInfo::OrgLevel { org_id: org_id.clone(), }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) @@ -745,7 +814,16 @@ pub mod routes { merchant_id: merchant_id.clone(), profile_ids: vec![profile_id.clone()], }; - let ex_rates = get_forex_exchange_rates(state.clone()).await?; + + let validator_response = request_validator( + AnalyticsRequest { + refund: Some(req.clone()), + ..Default::default() + }, + &state, + ) + .await?; + let ex_rates = validator_response; analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req) .await .map(ApplicationResponse::Json) diff --git a/crates/router/src/analytics_validator.rs b/crates/router/src/analytics_validator.rs new file mode 100644 index 0000000000..d50308cc84 --- /dev/null +++ b/crates/router/src/analytics_validator.rs @@ -0,0 +1,24 @@ +use analytics::errors::AnalyticsError; +use api_models::analytics::AnalyticsRequest; +use common_utils::errors::CustomResult; +use currency_conversion::types::ExchangeRates; +use router_env::logger; + +use crate::core::currency::get_forex_exchange_rates; + +pub async fn request_validator( + req_type: AnalyticsRequest, + state: &crate::routes::SessionState, +) -> CustomResult, AnalyticsError> { + let forex_enabled = state.conf.analytics.get_inner().get_forex_enabled(); + let require_forex_functionality = req_type.requires_forex_functionality(); + + let ex_rates = if forex_enabled && require_forex_functionality { + logger::info!("Fetching forex exchange rates"); + Some(get_forex_exchange_rates(state.clone()).await?) + } else { + None + }; + + Ok(ex_rates) +} diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 839dd47242..d43fa05431 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -16,6 +16,7 @@ pub mod workflows; #[cfg(feature = "olap")] pub mod analytics; +pub mod analytics_validator; pub mod events; pub mod middleware; pub mod services;