feat(analytics): Analytics Request Validator and config driven forex feature (#6733)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
Co-authored-by: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com>
This commit is contained in:
Uzair Khan
2024-12-17 15:53:00 +05:30
committed by GitHub
parent 94ad90f9ed
commit c883aa59aa
16 changed files with 428 additions and 160 deletions

View File

@ -661,6 +661,7 @@ pm_auth_key = "Some_pm_auth_key"
# Analytics configuration. # Analytics configuration.
[analytics] [analytics]
source = "sqlx" # The Analytics source/strategy to be used source = "sqlx" # The Analytics source/strategy to be used
forex_enabled = false # Enable or disable forex conversion for analytics
[analytics.clickhouse] [analytics.clickhouse]
username = "" # Clickhouse username username = "" # Clickhouse username

View File

@ -9,6 +9,7 @@ database_name = "clickhouse_db_name" # Clickhouse database name
# Analytics configuration. # Analytics configuration.
[analytics] [analytics]
source = "sqlx" # The Analytics source/strategy to be used source = "sqlx" # The Analytics source/strategy to be used
forex_enabled = false # Boolean to enable or disable forex conversion
[analytics.sqlx] [analytics.sqlx]
username = "db_user" # Analytics DB Username username = "db_user" # Analytics DB Username

View File

@ -719,6 +719,7 @@ authentication_analytics_topic = "hyperswitch-authentication-events"
[analytics] [analytics]
source = "sqlx" source = "sqlx"
forex_enabled = false
[analytics.clickhouse] [analytics.clickhouse]
username = "default" username = "default"

View File

@ -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`. - It will be in dashboard, labeled as `access key`.
### Configuring Forex APIs ### 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: To configure the Forex APIs, update the `config/development.toml` or `config/docker_compose.toml` file with your API keys:

View File

@ -969,21 +969,25 @@ impl AnalyticsProvider {
tenant: &dyn storage_impl::config::TenantConfig, tenant: &dyn storage_impl::config::TenantConfig,
) -> Self { ) -> Self {
match config { match config {
AnalyticsConfig::Sqlx { sqlx } => { AnalyticsConfig::Sqlx { sqlx, .. } => {
Self::Sqlx(SqlxClient::from_conf(sqlx, tenant.get_schema()).await) 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()), config: Arc::new(clickhouse.clone()),
database: tenant.get_clickhouse_database().to_string(), 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, SqlxClient::from_conf(sqlx, tenant.get_schema()).await,
ClickhouseClient { ClickhouseClient {
config: Arc::new(clickhouse.clone()), config: Arc::new(clickhouse.clone()),
database: tenant.get_clickhouse_database().to_string(), 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, SqlxClient::from_conf(sqlx, tenant.get_schema()).await,
ClickhouseClient { ClickhouseClient {
config: Arc::new(clickhouse.clone()), config: Arc::new(clickhouse.clone()),
@ -1000,20 +1004,35 @@ impl AnalyticsProvider {
pub enum AnalyticsConfig { pub enum AnalyticsConfig {
Sqlx { Sqlx {
sqlx: Database, sqlx: Database,
forex_enabled: bool,
}, },
Clickhouse { Clickhouse {
clickhouse: ClickhouseConfig, clickhouse: ClickhouseConfig,
forex_enabled: bool,
}, },
CombinedCkh { CombinedCkh {
sqlx: Database, sqlx: Database,
clickhouse: ClickhouseConfig, clickhouse: ClickhouseConfig,
forex_enabled: bool,
}, },
CombinedSqlx { CombinedSqlx {
sqlx: Database, sqlx: Database,
clickhouse: ClickhouseConfig, 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] #[async_trait::async_trait]
impl SecretsHandler for AnalyticsConfig { impl SecretsHandler for AnalyticsConfig {
async fn convert_to_raw_secret( async fn convert_to_raw_secret(
@ -1024,7 +1043,7 @@ impl SecretsHandler for AnalyticsConfig {
let decrypted_password = match analytics_config { let decrypted_password = match analytics_config {
// Todo: Perform kms decryption of clickhouse password // Todo: Perform kms decryption of clickhouse password
Self::Clickhouse { .. } => masking::Secret::new(String::default()), Self::Clickhouse { .. } => masking::Secret::new(String::default()),
Self::Sqlx { sqlx } Self::Sqlx { sqlx, .. }
| Self::CombinedCkh { sqlx, .. } | Self::CombinedCkh { sqlx, .. }
| Self::CombinedSqlx { sqlx, .. } => { | Self::CombinedSqlx { sqlx, .. } => {
secret_management_client secret_management_client
@ -1034,26 +1053,46 @@ impl SecretsHandler for AnalyticsConfig {
}; };
Ok(value.transition_state(|conf| match conf { Ok(value.transition_state(|conf| match conf {
Self::Sqlx { sqlx } => Self::Sqlx { Self::Sqlx {
sqlx,
forex_enabled,
} => Self::Sqlx {
sqlx: Database { sqlx: Database {
password: decrypted_password, password: decrypted_password,
..sqlx ..sqlx
}, },
forex_enabled,
}, },
Self::Clickhouse { clickhouse } => Self::Clickhouse { clickhouse }, Self::Clickhouse {
Self::CombinedCkh { sqlx, clickhouse } => Self::CombinedCkh { clickhouse,
forex_enabled,
} => Self::Clickhouse {
clickhouse,
forex_enabled,
},
Self::CombinedCkh {
sqlx,
clickhouse,
forex_enabled,
} => Self::CombinedCkh {
sqlx: Database { sqlx: Database {
password: decrypted_password, password: decrypted_password,
..sqlx ..sqlx
}, },
clickhouse, clickhouse,
forex_enabled,
}, },
Self::CombinedSqlx { sqlx, clickhouse } => Self::CombinedSqlx { Self::CombinedSqlx {
sqlx,
clickhouse,
forex_enabled,
} => Self::CombinedSqlx {
sqlx: Database { sqlx: Database {
password: decrypted_password, password: decrypted_password,
..sqlx ..sqlx
}, },
clickhouse, clickhouse,
forex_enabled,
}, },
})) }))
} }
@ -1063,6 +1102,7 @@ impl Default for AnalyticsConfig {
fn default() -> Self { fn default() -> Self {
Self::Sqlx { Self::Sqlx {
sqlx: Database::default(), sqlx: Database::default(),
forex_enabled: false,
} }
} }
} }

View File

@ -68,7 +68,7 @@ pub async fn get_sankey(
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn get_metrics( pub async fn get_metrics(
pool: &AnalyticsProvider, pool: &AnalyticsProvider,
ex_rates: &ExchangeRates, ex_rates: &Option<ExchangeRates>,
auth: &AuthInfo, auth: &AuthInfo,
req: GetPaymentIntentMetricRequest, req: GetPaymentIntentMetricRequest,
) -> AnalyticsResult<PaymentIntentsMetricsResponse<MetricsBucketResponse>> { ) -> AnalyticsResult<PaymentIntentsMetricsResponse<MetricsBucketResponse>> {
@ -201,22 +201,25 @@ pub async fn get_metrics(
total += total_count; total += total_count;
} }
if let Some(retried_amount) = collected_values.smart_retried_amount { if let Some(retried_amount) = collected_values.smart_retried_amount {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(retried_amount) i64::try_from(retried_amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.smart_retried_amount_in_usd = amount_in_usd; collected_values.smart_retried_amount_in_usd = amount_in_usd;
total_smart_retried_amount += retried_amount; total_smart_retried_amount += retried_amount;
total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0); 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) = if let Some(retried_amount) =
collected_values.smart_retried_amount_without_smart_retries collected_values.smart_retried_amount_without_smart_retries
{ {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(retried_amount) i64::try_from(retried_amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd; 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 += retried_amount;
total_smart_retried_amount_without_smart_retries_in_usd += total_smart_retried_amount_without_smart_retries_in_usd +=
amount_in_usd.unwrap_or(0); amount_in_usd.unwrap_or(0);
} }
if let Some(amount) = collected_values.payment_processed_amount { if let Some(amount) = collected_values.payment_processed_amount {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(amount) i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.payment_processed_amount_in_usd = amount_in_usd; 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_in_usd += amount_in_usd.unwrap_or(0);
total_payment_processed_amount += amount; total_payment_processed_amount += amount;
@ -270,22 +279,25 @@ pub async fn get_metrics(
total_payment_processed_count += count; total_payment_processed_count += count;
} }
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(amount) i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.payment_processed_amount_without_smart_retries_in_usd = collected_values.payment_processed_amount_without_smart_retries_in_usd =
amount_in_usd; amount_in_usd;
total_payment_processed_amount_without_smart_retries_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: Some(
total_payment_processed_amount_without_smart_retries, total_payment_processed_amount_without_smart_retries,
), ),
total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd), total_smart_retried_amount_in_usd: if ex_rates.is_some() {
total_smart_retried_amount_without_smart_retries_in_usd: Some( Some(total_smart_retried_amount_in_usd)
total_smart_retried_amount_without_smart_retries_in_usd, } else {
), None
total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd), },
total_payment_processed_amount_without_smart_retries_in_usd: Some( total_smart_retried_amount_without_smart_retries_in_usd: if ex_rates.is_some() {
total_payment_processed_amount_without_smart_retries_in_usd, 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: Some(total_payment_processed_count),
total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries: Some(
total_payment_processed_count_without_smart_retries, total_payment_processed_count_without_smart_retries,

View File

@ -48,7 +48,7 @@ pub enum TaskType {
#[instrument(skip_all)] #[instrument(skip_all)]
pub async fn get_metrics( pub async fn get_metrics(
pool: &AnalyticsProvider, pool: &AnalyticsProvider,
ex_rates: &ExchangeRates, ex_rates: &Option<ExchangeRates>,
auth: &AuthInfo, auth: &AuthInfo,
req: GetPaymentMetricRequest, req: GetPaymentMetricRequest,
) -> AnalyticsResult<PaymentsMetricsResponse<MetricsBucketResponse>> { ) -> AnalyticsResult<PaymentsMetricsResponse<MetricsBucketResponse>> {
@ -234,22 +234,25 @@ pub async fn get_metrics(
.map(|(id, val)| { .map(|(id, val)| {
let mut collected_values = val.collect(); let mut collected_values = val.collect();
if let Some(amount) = collected_values.payment_processed_amount { if let Some(amount) = collected_values.payment_processed_amount {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(amount) i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.payment_processed_amount_in_usd = amount_in_usd; collected_values.payment_processed_amount_in_usd = amount_in_usd;
total_payment_processed_amount += amount; total_payment_processed_amount += amount;
total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0); 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; total_payment_processed_count += count;
} }
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries { if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(amount) i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.payment_processed_amount_without_smart_retries_usd = amount_in_usd; 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 += amount;
total_payment_processed_amount_without_smart_retries_usd += total_payment_processed_amount_without_smart_retries_usd +=
@ -298,13 +304,19 @@ pub async fn get_metrics(
query_data, query_data,
meta_data: [PaymentsAnalyticsMetadata { meta_data: [PaymentsAnalyticsMetadata {
total_payment_processed_amount: Some(total_payment_processed_amount), 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: Some(
total_payment_processed_amount_without_smart_retries, total_payment_processed_amount_without_smart_retries,
), ),
total_payment_processed_amount_without_smart_retries_usd: Some( total_payment_processed_amount_without_smart_retries_usd: if ex_rates.is_some() {
total_payment_processed_amount_without_smart_retries_usd, Some(total_payment_processed_amount_without_smart_retries_usd)
), } else {
None
},
total_payment_processed_count: Some(total_payment_processed_count), total_payment_processed_count: Some(total_payment_processed_count),
total_payment_processed_count_without_smart_retries: Some( total_payment_processed_count_without_smart_retries: Some(
total_payment_processed_count_without_smart_retries, total_payment_processed_count_without_smart_retries,

View File

@ -47,7 +47,7 @@ pub enum TaskType {
pub async fn get_metrics( pub async fn get_metrics(
pool: &AnalyticsProvider, pool: &AnalyticsProvider,
ex_rates: &ExchangeRates, ex_rates: &Option<ExchangeRates>,
auth: &AuthInfo, auth: &AuthInfo,
req: GetRefundMetricRequest, req: GetRefundMetricRequest,
) -> AnalyticsResult<RefundsMetricsResponse<RefundMetricsBucketResponse>> { ) -> AnalyticsResult<RefundsMetricsResponse<RefundMetricsBucketResponse>> {
@ -217,22 +217,25 @@ pub async fn get_metrics(
total += total_count; total += total_count;
} }
if let Some(amount) = collected_values.refund_processed_amount { if let Some(amount) = collected_values.refund_processed_amount {
let amount_in_usd = id let amount_in_usd = if let Some(ex_rates) = ex_rates {
.currency id.currency
.and_then(|currency| { .and_then(|currency| {
i64::try_from(amount) i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e)) .inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok() .ok()
.and_then(|amount_i64| { .and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64) convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| { .inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e) logger::error!("Currency conversion error: {:?}", e)
}) })
.ok() .ok()
}) })
}) })
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64()) .map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default(); .unwrap_or_default()
} else {
None
};
collected_values.refund_processed_amount_in_usd = amount_in_usd; collected_values.refund_processed_amount_in_usd = amount_in_usd;
total_refund_processed_amount += amount; total_refund_processed_amount += amount;
total_refund_processed_amount_in_usd += amount_in_usd.unwrap_or(0); total_refund_processed_amount_in_usd += amount_in_usd.unwrap_or(0);
@ -261,7 +264,11 @@ pub async fn get_metrics(
meta_data: [RefundsAnalyticsMetadata { meta_data: [RefundsAnalyticsMetadata {
total_refund_success_rate, total_refund_success_rate,
total_refund_processed_amount: Some(total_refund_processed_amount), 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_processed_count: Some(total_refund_processed_count),
total_refund_reason_count: Some(total_refund_reason_count), total_refund_reason_count: Some(total_refund_reason_count),
total_refund_error_message_count: Some(total_refund_error_message_count), total_refund_error_message_count: Some(total_refund_error_message_count),

View File

@ -62,7 +62,42 @@ pub enum Granularity {
#[serde(rename = "G_ONEDAY")] #[serde(rename = "G_ONEDAY")]
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<GetPaymentIntentMetricRequest>,
pub payment_attempt: Option<GetPaymentMetricRequest>,
pub refund: Option<GetRefundMetricRequest>,
pub dispute: Option<GetDisputeMetricRequest>,
}
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)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct GetPaymentMetricRequest { pub struct GetPaymentMetricRequest {

View File

@ -3,7 +3,7 @@ use std::{
hash::{Hash, Hasher}, hash::{Hash, Hasher},
}; };
use super::{NameDescription, TimeRange}; use super::{ForexMetric, NameDescription, TimeRange};
use crate::enums::DisputeStage; use crate::enums::DisputeStage;
#[derive( #[derive(
@ -28,6 +28,14 @@ pub enum DisputeMetrics {
SessionizedTotalAmountDisputed, SessionizedTotalAmountDisputed,
SessionizedTotalDisputeLostAmount, SessionizedTotalDisputeLostAmount,
} }
impl ForexMetric for DisputeMetrics {
fn is_forex_metric(&self) -> bool {
matches!(
self,
Self::TotalAmountDisputed | Self::TotalDisputeLostAmount
)
}
}
#[derive( #[derive(
Debug, Debug,

View File

@ -5,7 +5,7 @@ use std::{
use common_utils::id_type; use common_utils::id_type;
use super::{NameDescription, TimeRange}; use super::{ForexMetric, NameDescription, TimeRange};
use crate::enums::{ use crate::enums::{
AuthenticationType, Connector, Currency, IntentStatus, PaymentMethod, PaymentMethodType, AuthenticationType, Connector, Currency, IntentStatus, PaymentMethod, PaymentMethodType,
}; };
@ -106,6 +106,17 @@ pub enum PaymentIntentMetrics {
SessionizedPaymentProcessedAmount, SessionizedPaymentProcessedAmount,
SessionizedPaymentsDistribution, 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)] #[derive(Debug, Default, serde::Serialize)]
pub struct ErrorResult { pub struct ErrorResult {

View File

@ -5,7 +5,7 @@ use std::{
use common_utils::id_type; use common_utils::id_type;
use super::{NameDescription, TimeRange}; use super::{ForexMetric, NameDescription, TimeRange};
use crate::enums::{ use crate::enums::{
AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod, AttemptStatus, AuthenticationType, CardNetwork, Connector, Currency, PaymentMethod,
PaymentMethodType, PaymentMethodType,
@ -119,6 +119,17 @@ pub enum PaymentMetrics {
FailureReasons, 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)] #[derive(Debug, Default, serde::Serialize)]
pub struct ErrorResult { pub struct ErrorResult {
pub reason: String, pub reason: String,

View File

@ -30,7 +30,7 @@ pub enum RefundType {
RetryRefund, RetryRefund,
} }
use super::{NameDescription, TimeRange}; use super::{ForexMetric, NameDescription, TimeRange};
#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)]
pub struct RefundFilters { pub struct RefundFilters {
#[serde(default)] #[serde(default)]
@ -137,6 +137,14 @@ pub enum RefundDistributions {
#[strum(serialize = "refund_error_message")] #[strum(serialize = "refund_error_message")]
SessionizedRefundErrorMessage, SessionizedRefundErrorMessage,
} }
impl ForexMetric for RefundMetrics {
fn is_forex_metric(&self) -> bool {
matches!(
self,
Self::RefundProcessedAmount | Self::SessionizedRefundProcessedAmount
)
}
}
pub mod metric_behaviour { pub mod metric_behaviour {
pub struct RefundSuccessRate; pub struct RefundSuccessRate;

View File

@ -18,12 +18,12 @@ pub mod routes {
search::{ search::{
GetGlobalSearchRequest, GetSearchRequest, GetSearchRequestWithIndex, SearchIndex, GetGlobalSearchRequest, GetSearchRequest, GetSearchRequestWithIndex, SearchIndex,
}, },
GenerateReportRequest, GetActivePaymentsMetricRequest, GetApiEventFiltersRequest, AnalyticsRequest, GenerateReportRequest, GetActivePaymentsMetricRequest,
GetApiEventMetricRequest, GetAuthEventMetricRequest, GetDisputeMetricRequest, GetApiEventFiltersRequest, GetApiEventMetricRequest, GetAuthEventMetricRequest,
GetFrmFilterRequest, GetFrmMetricRequest, GetPaymentFiltersRequest, GetDisputeMetricRequest, GetFrmFilterRequest, GetFrmMetricRequest,
GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest, GetPaymentMetricRequest, GetPaymentFiltersRequest, GetPaymentIntentFiltersRequest, GetPaymentIntentMetricRequest,
GetRefundFilterRequest, GetRefundMetricRequest, GetSdkEventFiltersRequest, GetPaymentMetricRequest, GetRefundFilterRequest, GetRefundMetricRequest,
GetSdkEventMetricRequest, ReportRequest, GetSdkEventFiltersRequest, GetSdkEventMetricRequest, ReportRequest,
}; };
use common_enums::EntityType; use common_enums::EntityType;
use common_utils::types::TimeRange; use common_utils::types::TimeRange;
@ -31,11 +31,9 @@ pub mod routes {
use futures::{stream::FuturesUnordered, StreamExt}; use futures::{stream::FuturesUnordered, StreamExt};
use crate::{ use crate::{
analytics_validator::request_validator,
consts::opensearch::SEARCH_INDEXES, consts::opensearch::SEARCH_INDEXES,
core::{ core::{api_locking, errors::user::UserErrors, verification::utils},
api_locking, currency::get_forex_exchange_rates, errors::user::UserErrors,
verification::utils,
},
db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload}, db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload},
routes::AppState, routes::AppState,
services::{ services::{
@ -405,7 +403,15 @@ pub mod routes {
org_id: org_id.clone(), org_id: org_id.clone(),
merchant_ids: vec![merchant_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) analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -444,7 +450,16 @@ pub mod routes {
let auth: AuthInfo = AuthInfo::OrgLevel { let auth: AuthInfo = AuthInfo::OrgLevel {
org_id: org_id.clone(), 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) analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -491,7 +506,16 @@ pub mod routes {
merchant_id: merchant_id.clone(), merchant_id: merchant_id.clone(),
profile_ids: vec![profile_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) analytics::payments::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -532,7 +556,16 @@ pub mod routes {
org_id: org_id.clone(), org_id: org_id.clone(),
merchant_ids: vec![merchant_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) analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -571,7 +604,16 @@ pub mod routes {
let auth: AuthInfo = AuthInfo::OrgLevel { let auth: AuthInfo = AuthInfo::OrgLevel {
org_id: org_id.clone(), 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) analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -618,7 +660,16 @@ pub mod routes {
merchant_id: merchant_id.clone(), merchant_id: merchant_id.clone(),
profile_ids: vec![profile_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) analytics::payment_intents::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -659,7 +710,16 @@ pub mod routes {
org_id: org_id.clone(), org_id: org_id.clone(),
merchant_ids: vec![merchant_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) analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -698,7 +758,16 @@ pub mod routes {
let auth: AuthInfo = AuthInfo::OrgLevel { let auth: AuthInfo = AuthInfo::OrgLevel {
org_id: org_id.clone(), 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) analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)
@ -745,7 +814,16 @@ pub mod routes {
merchant_id: merchant_id.clone(), merchant_id: merchant_id.clone(),
profile_ids: vec![profile_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) analytics::refunds::get_metrics(&state.pool, &ex_rates, &auth, req)
.await .await
.map(ApplicationResponse::Json) .map(ApplicationResponse::Json)

View File

@ -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<Option<ExchangeRates>, 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)
}

View File

@ -16,6 +16,7 @@ pub mod workflows;
#[cfg(feature = "olap")] #[cfg(feature = "olap")]
pub mod analytics; pub mod analytics;
pub mod analytics_validator;
pub mod events; pub mod events;
pub mod middleware; pub mod middleware;
pub mod services; pub mod services;