feat(analytics): implement currency conversion to power multi-currency aggregation (#6418)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Uzair Khan
2024-11-06 14:46:16 +05:30
committed by GitHub
parent ae4df051cc
commit 01c5216fdd
18 changed files with 273 additions and 24 deletions

View File

@ -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"] }

View File

@ -12,6 +12,8 @@ pub enum AnalyticsError {
UnknownError,
#[error("Access Forbidden Analytics Error")]
AccessForbiddenError,
#[error("Failed to fetch currency exchange rate")]
ForexFetchFailed,
}
impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
@ -32,6 +34,12 @@ impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
Self::AccessForbiddenError => {
ApiErrorResponse::Unauthorized(ApiError::new("IR", 0, "Access Forbidden", None))
}
Self::ForexFetchFailed => ApiErrorResponse::InternalServerError(ApiError::new(
"HE",
0,
"Failed to fetch currency exchange rate",
None,
)),
}
}
}

View File

@ -86,7 +86,7 @@ impl PaymentIntentMetricAccumulator for CountAccumulator {
}
impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
type MetricOutput = (Option<u64>, Option<u64>);
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
#[inline]
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
self.amount = match (
@ -117,7 +117,7 @@ impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
.amount_without_retries
.and_then(|i| u64::try_from(i).ok())
.or(Some(0));
(with_retries, without_retries)
(with_retries, without_retries, Some(0), Some(0))
}
}
@ -185,7 +185,14 @@ impl PaymentIntentMetricAccumulator for PaymentsSuccessRateAccumulator {
}
impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
type MetricOutput = (
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
);
#[inline]
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
self.total_with_retries = match (
@ -235,6 +242,8 @@ impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
count_with_retries,
total_without_retries,
count_without_retries,
Some(0),
Some(0),
)
}
}
@ -301,13 +310,19 @@ impl PaymentIntentMetricsAccumulator {
payments_success_rate,
payments_success_rate_without_smart_retries,
) = self.payments_success_rate.collect();
let (smart_retried_amount, smart_retried_amount_without_smart_retries) =
self.smart_retried_amount.collect();
let (
smart_retried_amount,
smart_retried_amount_without_smart_retries,
smart_retried_amount_in_usd,
smart_retried_amount_without_smart_retries_in_usd,
) = self.smart_retried_amount.collect();
let (
payment_processed_amount,
payment_processed_count,
payment_processed_amount_without_smart_retries,
payment_processed_count_without_smart_retries,
payment_processed_amount_in_usd,
payment_processed_amount_without_smart_retries_in_usd,
) = self.payment_processed_amount.collect();
let (
payments_success_rate_distribution_without_smart_retries,
@ -317,7 +332,9 @@ impl PaymentIntentMetricsAccumulator {
successful_smart_retries: self.successful_smart_retries.collect(),
total_smart_retries: self.total_smart_retries.collect(),
smart_retried_amount,
smart_retried_amount_in_usd,
smart_retried_amount_without_smart_retries,
smart_retried_amount_without_smart_retries_in_usd,
payment_intent_count: self.payment_intent_count.collect(),
successful_payments,
successful_payments_without_smart_retries,
@ -330,6 +347,8 @@ impl PaymentIntentMetricsAccumulator {
payment_processed_count_without_smart_retries,
payments_success_rate_distribution_without_smart_retries,
payments_failure_rate_distribution_without_smart_retries,
payment_processed_amount_in_usd,
payment_processed_amount_without_smart_retries_in_usd,
}
}
}

View File

@ -10,8 +10,10 @@ use api_models::analytics::{
PaymentIntentFiltersResponse, PaymentIntentsAnalyticsMetadata, PaymentIntentsMetricsResponse,
SankeyResponse,
};
use common_enums::IntentStatus;
use bigdecimal::ToPrimitive;
use common_enums::{Currency, IntentStatus};
use common_utils::{errors::CustomResult, types::TimeRange};
use currency_conversion::{conversion::convert, types::ExchangeRates};
use error_stack::ResultExt;
use router_env::{
instrument, logger,
@ -120,6 +122,7 @@ pub async fn get_sankey(
#[instrument(skip_all)]
pub async fn get_metrics(
pool: &AnalyticsProvider,
ex_rates: &ExchangeRates,
auth: &AuthInfo,
req: GetPaymentIntentMetricRequest,
) -> AnalyticsResult<PaymentIntentsMetricsResponse<MetricsBucketResponse>> {
@ -227,16 +230,20 @@ pub async fn get_metrics(
let mut success = 0;
let mut success_without_smart_retries = 0;
let mut total_smart_retried_amount = 0;
let mut total_smart_retried_amount_in_usd = 0;
let mut total_smart_retried_amount_without_smart_retries = 0;
let mut total_smart_retried_amount_without_smart_retries_in_usd = 0;
let mut total = 0;
let mut total_payment_processed_amount = 0;
let mut total_payment_processed_amount_in_usd = 0;
let mut total_payment_processed_count = 0;
let mut total_payment_processed_amount_without_smart_retries = 0;
let mut total_payment_processed_amount_without_smart_retries_in_usd = 0;
let mut total_payment_processed_count_without_smart_retries = 0;
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
.into_iter()
.map(|(id, val)| {
let collected_values = val.collect();
let mut collected_values = val.collect();
if let Some(success_count) = collected_values.successful_payments {
success += success_count;
}
@ -248,20 +255,95 @@ pub async fn get_metrics(
total += total_count;
}
if let Some(retried_amount) = collected_values.smart_retried_amount {
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(retried_amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.smart_retried_amount_in_usd = amount_in_usd;
total_smart_retried_amount += retried_amount;
total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0);
}
if let Some(retried_amount) =
collected_values.smart_retried_amount_without_smart_retries
{
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(retried_amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd;
total_smart_retried_amount_without_smart_retries += retried_amount;
total_smart_retried_amount_without_smart_retries_in_usd +=
amount_in_usd.unwrap_or(0);
}
if let Some(amount) = collected_values.payment_processed_amount {
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.payment_processed_amount_in_usd = amount_in_usd;
total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0);
total_payment_processed_amount += amount;
}
if let Some(count) = collected_values.payment_processed_count {
total_payment_processed_count += count;
}
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.payment_processed_amount_without_smart_retries_in_usd =
amount_in_usd;
total_payment_processed_amount_without_smart_retries_in_usd +=
amount_in_usd.unwrap_or(0);
total_payment_processed_amount_without_smart_retries += amount;
}
if let Some(count) = collected_values.payment_processed_count_without_smart_retries {
@ -294,6 +376,14 @@ pub async fn get_metrics(
total_payment_processed_amount_without_smart_retries: Some(
total_payment_processed_amount_without_smart_retries,
),
total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd),
total_smart_retried_amount_without_smart_retries_in_usd: Some(
total_smart_retried_amount_without_smart_retries_in_usd,
),
total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd),
total_payment_processed_amount_without_smart_retries_in_usd: Some(
total_payment_processed_amount_without_smart_retries_in_usd,
),
total_payment_processed_count: Some(total_payment_processed_count),
total_payment_processed_count_without_smart_retries: Some(
total_payment_processed_count_without_smart_retries,

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -272,7 +272,14 @@ impl PaymentMetricAccumulator for CountAccumulator {
}
impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
type MetricOutput = (
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
Option<u64>,
);
#[inline]
fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) {
self.total_with_retries = match (
@ -322,6 +329,8 @@ impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
count_with_retries,
total_without_retries,
count_without_retries,
Some(0),
Some(0),
)
}
}
@ -378,6 +387,8 @@ impl PaymentMetricsAccumulator {
payment_processed_count,
payment_processed_amount_without_smart_retries,
payment_processed_count_without_smart_retries,
payment_processed_amount_usd,
payment_processed_amount_without_smart_retries_usd,
) = self.processed_amount.collect();
let (
payments_success_rate_distribution,
@ -406,6 +417,8 @@ impl PaymentMetricsAccumulator {
payments_failure_rate_distribution_without_smart_retries,
failure_reason_count,
failure_reason_count_without_smart_retries,
payment_processed_amount_usd,
payment_processed_amount_without_smart_retries_usd,
}
}
}

View File

@ -9,7 +9,10 @@ use api_models::analytics::{
FilterValue, GetPaymentFiltersRequest, GetPaymentMetricRequest, PaymentFiltersResponse,
PaymentsAnalyticsMetadata, PaymentsMetricsResponse,
};
use bigdecimal::ToPrimitive;
use common_enums::Currency;
use common_utils::errors::CustomResult;
use currency_conversion::{conversion::convert, types::ExchangeRates};
use error_stack::ResultExt;
use router_env::{
instrument, logger,
@ -46,6 +49,7 @@ pub enum TaskType {
#[instrument(skip_all)]
pub async fn get_metrics(
pool: &AnalyticsProvider,
ex_rates: &ExchangeRates,
auth: &AuthInfo,
req: GetPaymentMetricRequest,
) -> AnalyticsResult<PaymentsMetricsResponse<MetricsBucketResponse>> {
@ -224,18 +228,57 @@ pub async fn get_metrics(
let mut total_payment_processed_count_without_smart_retries = 0;
let mut total_failure_reasons_count = 0;
let mut total_failure_reasons_count_without_smart_retries = 0;
let mut total_payment_processed_amount_usd = 0;
let mut total_payment_processed_amount_without_smart_retries_usd = 0;
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
.into_iter()
.map(|(id, val)| {
let collected_values = val.collect();
let mut collected_values = val.collect();
if let Some(amount) = collected_values.payment_processed_amount {
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.payment_processed_amount_usd = amount_in_usd;
total_payment_processed_amount += amount;
total_payment_processed_amount_usd += amount_in_usd.unwrap_or(0);
}
if let Some(count) = collected_values.payment_processed_count {
total_payment_processed_count += count;
}
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
let amount_in_usd = id
.currency
.and_then(|currency| {
i64::try_from(amount)
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
.ok()
.and_then(|amount_i64| {
convert(ex_rates, currency, Currency::USD, amount_i64)
.inspect_err(|e| {
logger::error!("Currency conversion error: {:?}", e)
})
.ok()
})
})
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
.unwrap_or_default();
collected_values.payment_processed_amount_without_smart_retries_usd = amount_in_usd;
total_payment_processed_amount_without_smart_retries += amount;
total_payment_processed_amount_without_smart_retries_usd +=
amount_in_usd.unwrap_or(0);
}
if let Some(count) = collected_values.payment_processed_count_without_smart_retries {
total_payment_processed_count_without_smart_retries += count;
@ -252,14 +295,17 @@ pub async fn get_metrics(
}
})
.collect();
Ok(PaymentsMetricsResponse {
query_data,
meta_data: [PaymentsAnalyticsMetadata {
total_payment_processed_amount: Some(total_payment_processed_amount),
total_payment_processed_amount_usd: Some(total_payment_processed_amount_usd),
total_payment_processed_amount_without_smart_retries: Some(
total_payment_processed_amount_without_smart_retries,
),
total_payment_processed_amount_without_smart_retries_usd: Some(
total_payment_processed_amount_without_smart_retries_usd,
),
total_payment_processed_count: Some(total_payment_processed_count),
total_payment_processed_count_without_smart_retries: Some(
total_payment_processed_count_without_smart_retries,

View File

@ -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)

View File

@ -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)

View File

@ -203,7 +203,9 @@ pub struct AnalyticsMetadata {
#[derive(Debug, serde::Serialize)]
pub struct PaymentsAnalyticsMetadata {
pub total_payment_processed_amount: Option<u64>,
pub total_payment_processed_amount_usd: Option<u64>,
pub total_payment_processed_amount_without_smart_retries: Option<u64>,
pub total_payment_processed_amount_without_smart_retries_usd: Option<u64>,
pub total_payment_processed_count: Option<u64>,
pub total_payment_processed_count_without_smart_retries: Option<u64>,
pub total_failure_reasons_count: Option<u64>,
@ -218,6 +220,10 @@ pub struct PaymentIntentsAnalyticsMetadata {
pub total_smart_retried_amount_without_smart_retries: Option<u64>,
pub total_payment_processed_amount: Option<u64>,
pub total_payment_processed_amount_without_smart_retries: Option<u64>,
pub total_smart_retried_amount_in_usd: Option<u64>,
pub total_smart_retried_amount_without_smart_retries_in_usd: Option<u64>,
pub total_payment_processed_amount_in_usd: Option<u64>,
pub total_payment_processed_amount_without_smart_retries_in_usd: Option<u64>,
pub total_payment_processed_count: Option<u64>,
pub total_payment_processed_count_without_smart_retries: Option<u64>,
}

View File

@ -161,7 +161,9 @@ pub struct PaymentIntentMetricsBucketValue {
pub successful_smart_retries: Option<u64>,
pub total_smart_retries: Option<u64>,
pub smart_retried_amount: Option<u64>,
pub smart_retried_amount_in_usd: Option<u64>,
pub smart_retried_amount_without_smart_retries: Option<u64>,
pub smart_retried_amount_without_smart_retries_in_usd: Option<u64>,
pub payment_intent_count: Option<u64>,
pub successful_payments: Option<u32>,
pub successful_payments_without_smart_retries: Option<u32>,
@ -169,8 +171,10 @@ pub struct PaymentIntentMetricsBucketValue {
pub payments_success_rate: Option<f64>,
pub payments_success_rate_without_smart_retries: Option<f64>,
pub payment_processed_amount: Option<u64>,
pub payment_processed_amount_in_usd: Option<u64>,
pub payment_processed_count: Option<u64>,
pub payment_processed_amount_without_smart_retries: Option<u64>,
pub payment_processed_amount_without_smart_retries_in_usd: Option<u64>,
pub payment_processed_count_without_smart_retries: Option<u64>,
pub payments_success_rate_distribution_without_smart_retries: Option<f64>,
pub payments_failure_rate_distribution_without_smart_retries: Option<f64>,

View File

@ -271,8 +271,10 @@ pub struct PaymentMetricsBucketValue {
pub payment_count: Option<u64>,
pub payment_success_count: Option<u64>,
pub payment_processed_amount: Option<u64>,
pub payment_processed_amount_usd: Option<u64>,
pub payment_processed_count: Option<u64>,
pub payment_processed_amount_without_smart_retries: Option<u64>,
pub payment_processed_amount_without_smart_retries_usd: Option<u64>,
pub payment_processed_count_without_smart_retries: Option<u64>,
pub avg_ticket_size: Option<f64>,
pub payment_error_message: Option<Vec<ErrorResult>>,

View File

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

View File

@ -1,4 +1,6 @@
use analytics::errors::AnalyticsError;
use common_utils::errors::CustomResult;
use currency_conversion::types::ExchangeRates;
use error_stack::ResultExt;
use crate::{
@ -46,3 +48,19 @@ pub async fn convert_forex(
.change_context(ApiErrorResponse::InternalServerError)?,
))
}
pub async fn get_forex_exchange_rates(
state: SessionState,
) -> CustomResult<ExchangeRates, AnalyticsError> {
let forex_api = state.conf.forex_api.get_inner();
let rates = get_forex_rates(
&state,
forex_api.call_delay,
forex_api.local_fetch_retry_delay,
forex_api.local_fetch_retry_count,
)
.await
.change_context(AnalyticsError::ForexFetchFailed)?;
Ok((*rates.data).clone())
}

View File

@ -26,7 +26,7 @@ const FALLBACK_FOREX_API_CURRENCY_PREFIX: &str = "USD";
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct FxExchangeRatesCacheEntry {
data: Arc<ExchangeRates>,
pub data: Arc<ExchangeRates>,
timestamp: i64,
}
@ -421,7 +421,13 @@ pub async fn fallback_fetch_forex_rates(
conversions.insert(enum_curr, currency_factors);
}
None => {
logger::error!("Rates for {} not received from API", &enum_curr);
if enum_curr == enums::Currency::USD {
let currency_factors =
CurrencyFactors::new(Decimal::new(1, 0), Decimal::new(1, 0));
conversions.insert(enum_curr, currency_factors);
} else {
logger::error!("Rates for {} not received from API", &enum_curr);
}
}
};
}