Skip to content

Commit 9d32765

Browse files
maveroxhyperswitch-bot[bot]
authored andcommitted
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>
1 parent b75c882 commit 9d32765

File tree

18 files changed

+273
-24
lines changed

18 files changed

+273
-24
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/analytics/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ hyperswitch_interfaces = { version = "0.1.0", path = "../hyperswitch_interfaces"
2121
masking = { version = "0.1.0", path = "../masking" }
2222
router_env = { version = "0.1.0", path = "../router_env", features = ["log_extra_implicit_fields", "log_custom_entries_to_extra"] }
2323
storage_impl = { version = "0.1.0", path = "../storage_impl", default-features = false }
24+
currency_conversion = { version = "0.1.0", path = "../currency_conversion" }
2425

2526
#Third Party dependencies
2627
actix-web = "4.5.1"
@@ -34,6 +35,7 @@ futures = "0.3.30"
3435
once_cell = "1.19.0"
3536
opensearch = { version = "2.2.0", features = ["aws-auth"] }
3637
reqwest = { version = "0.11.27", features = ["serde_json"] }
38+
rust_decimal = "1.35"
3739
serde = { version = "1.0.197", features = ["derive", "rc"] }
3840
serde_json = "1.0.115"
3941
sqlx = { version = "0.8.2", features = ["postgres", "runtime-tokio", "runtime-tokio-native-tls", "time", "bigdecimal"] }

crates/analytics/src/errors.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ pub enum AnalyticsError {
1212
UnknownError,
1313
#[error("Access Forbidden Analytics Error")]
1414
AccessForbiddenError,
15+
#[error("Failed to fetch currency exchange rate")]
16+
ForexFetchFailed,
1517
}
1618

1719
impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
@@ -32,6 +34,12 @@ impl ErrorSwitch<ApiErrorResponse> for AnalyticsError {
3234
Self::AccessForbiddenError => {
3335
ApiErrorResponse::Unauthorized(ApiError::new("IR", 0, "Access Forbidden", None))
3436
}
37+
Self::ForexFetchFailed => ApiErrorResponse::InternalServerError(ApiError::new(
38+
"HE",
39+
0,
40+
"Failed to fetch currency exchange rate",
41+
None,
42+
)),
3543
}
3644
}
3745
}

crates/analytics/src/payment_intents/accumulator.rs

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ impl PaymentIntentMetricAccumulator for CountAccumulator {
8686
}
8787

8888
impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
89-
type MetricOutput = (Option<u64>, Option<u64>);
89+
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
9090
#[inline]
9191
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
9292
self.amount = match (
@@ -117,7 +117,7 @@ impl PaymentIntentMetricAccumulator for SmartRetriedAmountAccumulator {
117117
.amount_without_retries
118118
.and_then(|i| u64::try_from(i).ok())
119119
.or(Some(0));
120-
(with_retries, without_retries)
120+
(with_retries, without_retries, Some(0), Some(0))
121121
}
122122
}
123123

@@ -185,7 +185,14 @@ impl PaymentIntentMetricAccumulator for PaymentsSuccessRateAccumulator {
185185
}
186186

187187
impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
188-
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
188+
type MetricOutput = (
189+
Option<u64>,
190+
Option<u64>,
191+
Option<u64>,
192+
Option<u64>,
193+
Option<u64>,
194+
Option<u64>,
195+
);
189196
#[inline]
190197
fn add_metrics_bucket(&mut self, metrics: &PaymentIntentMetricRow) {
191198
self.total_with_retries = match (
@@ -235,6 +242,8 @@ impl PaymentIntentMetricAccumulator for ProcessedAmountAccumulator {
235242
count_with_retries,
236243
total_without_retries,
237244
count_without_retries,
245+
Some(0),
246+
Some(0),
238247
)
239248
}
240249
}
@@ -301,13 +310,19 @@ impl PaymentIntentMetricsAccumulator {
301310
payments_success_rate,
302311
payments_success_rate_without_smart_retries,
303312
) = self.payments_success_rate.collect();
304-
let (smart_retried_amount, smart_retried_amount_without_smart_retries) =
305-
self.smart_retried_amount.collect();
313+
let (
314+
smart_retried_amount,
315+
smart_retried_amount_without_smart_retries,
316+
smart_retried_amount_in_usd,
317+
smart_retried_amount_without_smart_retries_in_usd,
318+
) = self.smart_retried_amount.collect();
306319
let (
307320
payment_processed_amount,
308321
payment_processed_count,
309322
payment_processed_amount_without_smart_retries,
310323
payment_processed_count_without_smart_retries,
324+
payment_processed_amount_in_usd,
325+
payment_processed_amount_without_smart_retries_in_usd,
311326
) = self.payment_processed_amount.collect();
312327
let (
313328
payments_success_rate_distribution_without_smart_retries,
@@ -317,7 +332,9 @@ impl PaymentIntentMetricsAccumulator {
317332
successful_smart_retries: self.successful_smart_retries.collect(),
318333
total_smart_retries: self.total_smart_retries.collect(),
319334
smart_retried_amount,
335+
smart_retried_amount_in_usd,
320336
smart_retried_amount_without_smart_retries,
337+
smart_retried_amount_without_smart_retries_in_usd,
321338
payment_intent_count: self.payment_intent_count.collect(),
322339
successful_payments,
323340
successful_payments_without_smart_retries,
@@ -330,6 +347,8 @@ impl PaymentIntentMetricsAccumulator {
330347
payment_processed_count_without_smart_retries,
331348
payments_success_rate_distribution_without_smart_retries,
332349
payments_failure_rate_distribution_without_smart_retries,
350+
payment_processed_amount_in_usd,
351+
payment_processed_amount_without_smart_retries_in_usd,
333352
}
334353
}
335354
}

crates/analytics/src/payment_intents/core.rs

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ use api_models::analytics::{
1010
PaymentIntentFiltersResponse, PaymentIntentsAnalyticsMetadata, PaymentIntentsMetricsResponse,
1111
SankeyResponse,
1212
};
13-
use common_enums::IntentStatus;
13+
use bigdecimal::ToPrimitive;
14+
use common_enums::{Currency, IntentStatus};
1415
use common_utils::{errors::CustomResult, types::TimeRange};
16+
use currency_conversion::{conversion::convert, types::ExchangeRates};
1517
use error_stack::ResultExt;
1618
use router_env::{
1719
instrument, logger,
@@ -120,6 +122,7 @@ pub async fn get_sankey(
120122
#[instrument(skip_all)]
121123
pub async fn get_metrics(
122124
pool: &AnalyticsProvider,
125+
ex_rates: &ExchangeRates,
123126
auth: &AuthInfo,
124127
req: GetPaymentIntentMetricRequest,
125128
) -> AnalyticsResult<PaymentIntentsMetricsResponse<MetricsBucketResponse>> {
@@ -227,16 +230,20 @@ pub async fn get_metrics(
227230
let mut success = 0;
228231
let mut success_without_smart_retries = 0;
229232
let mut total_smart_retried_amount = 0;
233+
let mut total_smart_retried_amount_in_usd = 0;
230234
let mut total_smart_retried_amount_without_smart_retries = 0;
235+
let mut total_smart_retried_amount_without_smart_retries_in_usd = 0;
231236
let mut total = 0;
232237
let mut total_payment_processed_amount = 0;
238+
let mut total_payment_processed_amount_in_usd = 0;
233239
let mut total_payment_processed_count = 0;
234240
let mut total_payment_processed_amount_without_smart_retries = 0;
241+
let mut total_payment_processed_amount_without_smart_retries_in_usd = 0;
235242
let mut total_payment_processed_count_without_smart_retries = 0;
236243
let query_data: Vec<MetricsBucketResponse> = metrics_accumulator
237244
.into_iter()
238245
.map(|(id, val)| {
239-
let collected_values = val.collect();
246+
let mut collected_values = val.collect();
240247
if let Some(success_count) = collected_values.successful_payments {
241248
success += success_count;
242249
}
@@ -248,20 +255,95 @@ pub async fn get_metrics(
248255
total += total_count;
249256
}
250257
if let Some(retried_amount) = collected_values.smart_retried_amount {
258+
let amount_in_usd = id
259+
.currency
260+
.and_then(|currency| {
261+
i64::try_from(retried_amount)
262+
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
263+
.ok()
264+
.and_then(|amount_i64| {
265+
convert(ex_rates, currency, Currency::USD, amount_i64)
266+
.inspect_err(|e| {
267+
logger::error!("Currency conversion error: {:?}", e)
268+
})
269+
.ok()
270+
})
271+
})
272+
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
273+
.unwrap_or_default();
274+
collected_values.smart_retried_amount_in_usd = amount_in_usd;
251275
total_smart_retried_amount += retried_amount;
276+
total_smart_retried_amount_in_usd += amount_in_usd.unwrap_or(0);
252277
}
253278
if let Some(retried_amount) =
254279
collected_values.smart_retried_amount_without_smart_retries
255280
{
281+
let amount_in_usd = id
282+
.currency
283+
.and_then(|currency| {
284+
i64::try_from(retried_amount)
285+
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
286+
.ok()
287+
.and_then(|amount_i64| {
288+
convert(ex_rates, currency, Currency::USD, amount_i64)
289+
.inspect_err(|e| {
290+
logger::error!("Currency conversion error: {:?}", e)
291+
})
292+
.ok()
293+
})
294+
})
295+
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
296+
.unwrap_or_default();
297+
collected_values.smart_retried_amount_without_smart_retries_in_usd = amount_in_usd;
256298
total_smart_retried_amount_without_smart_retries += retried_amount;
299+
total_smart_retried_amount_without_smart_retries_in_usd +=
300+
amount_in_usd.unwrap_or(0);
257301
}
258302
if let Some(amount) = collected_values.payment_processed_amount {
303+
let amount_in_usd = id
304+
.currency
305+
.and_then(|currency| {
306+
i64::try_from(amount)
307+
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
308+
.ok()
309+
.and_then(|amount_i64| {
310+
convert(ex_rates, currency, Currency::USD, amount_i64)
311+
.inspect_err(|e| {
312+
logger::error!("Currency conversion error: {:?}", e)
313+
})
314+
.ok()
315+
})
316+
})
317+
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
318+
.unwrap_or_default();
319+
collected_values.payment_processed_amount_in_usd = amount_in_usd;
320+
total_payment_processed_amount_in_usd += amount_in_usd.unwrap_or(0);
259321
total_payment_processed_amount += amount;
260322
}
261323
if let Some(count) = collected_values.payment_processed_count {
262324
total_payment_processed_count += count;
263325
}
264326
if let Some(amount) = collected_values.payment_processed_amount_without_smart_retries {
327+
let amount_in_usd = id
328+
.currency
329+
.and_then(|currency| {
330+
i64::try_from(amount)
331+
.inspect_err(|e| logger::error!("Amount conversion error: {:?}", e))
332+
.ok()
333+
.and_then(|amount_i64| {
334+
convert(ex_rates, currency, Currency::USD, amount_i64)
335+
.inspect_err(|e| {
336+
logger::error!("Currency conversion error: {:?}", e)
337+
})
338+
.ok()
339+
})
340+
})
341+
.map(|amount| (amount * rust_decimal::Decimal::new(100, 0)).to_u64())
342+
.unwrap_or_default();
343+
collected_values.payment_processed_amount_without_smart_retries_in_usd =
344+
amount_in_usd;
345+
total_payment_processed_amount_without_smart_retries_in_usd +=
346+
amount_in_usd.unwrap_or(0);
265347
total_payment_processed_amount_without_smart_retries += amount;
266348
}
267349
if let Some(count) = collected_values.payment_processed_count_without_smart_retries {
@@ -294,6 +376,14 @@ pub async fn get_metrics(
294376
total_payment_processed_amount_without_smart_retries: Some(
295377
total_payment_processed_amount_without_smart_retries,
296378
),
379+
total_smart_retried_amount_in_usd: Some(total_smart_retried_amount_in_usd),
380+
total_smart_retried_amount_without_smart_retries_in_usd: Some(
381+
total_smart_retried_amount_without_smart_retries_in_usd,
382+
),
383+
total_payment_processed_amount_in_usd: Some(total_payment_processed_amount_in_usd),
384+
total_payment_processed_amount_without_smart_retries_in_usd: Some(
385+
total_payment_processed_amount_without_smart_retries_in_usd,
386+
),
297387
total_payment_processed_count: Some(total_payment_processed_count),
298388
total_payment_processed_count_without_smart_retries: Some(
299389
total_payment_processed_count_without_smart_retries,

crates/analytics/src/payment_intents/metrics/sessionized_metrics/payment_processed_amount.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ where
6161
query_builder
6262
.add_select_column("attempt_count == 1 as first_attempt")
6363
.switch()?;
64-
64+
query_builder.add_select_column("currency").switch()?;
6565
query_builder
6666
.add_select_column(Aggregate::Sum {
6767
field: "amount",
@@ -101,7 +101,10 @@ where
101101
.add_group_by_clause("attempt_count")
102102
.attach_printable("Error grouping by attempt_count")
103103
.switch()?;
104-
104+
query_builder
105+
.add_group_by_clause("currency")
106+
.attach_printable("Error grouping by currency")
107+
.switch()?;
105108
if let Some(granularity) = granularity.as_ref() {
106109
granularity
107110
.set_group_by_clause(&mut query_builder)

crates/analytics/src/payment_intents/metrics/sessionized_metrics/smart_retried_amount.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ where
6363
.add_select_column("attempt_count == 1 as first_attempt")
6464
.switch()?;
6565

66+
query_builder.add_select_column("currency").switch()?;
6667
query_builder
6768
.add_select_column(Aggregate::Min {
6869
field: "created_at",
@@ -102,7 +103,10 @@ where
102103
.add_group_by_clause("first_attempt")
103104
.attach_printable("Error grouping by first_attempt")
104105
.switch()?;
105-
106+
query_builder
107+
.add_group_by_clause("currency")
108+
.attach_printable("Error grouping by first_attempt")
109+
.switch()?;
106110
if let Some(granularity) = granularity.as_ref() {
107111
granularity
108112
.set_group_by_clause(&mut query_builder)

crates/analytics/src/payment_intents/metrics/smart_retried_amount.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ where
6262
query_builder
6363
.add_select_column("attempt_count == 1 as first_attempt")
6464
.switch()?;
65-
65+
query_builder.add_select_column("currency").switch()?;
6666
query_builder
6767
.add_select_column(Aggregate::Min {
6868
field: "created_at",
@@ -102,7 +102,10 @@ where
102102
.add_group_by_clause("first_attempt")
103103
.attach_printable("Error grouping by first_attempt")
104104
.switch()?;
105-
105+
query_builder
106+
.add_group_by_clause("currency")
107+
.attach_printable("Error grouping by currency")
108+
.switch()?;
106109
if let Some(granularity) = granularity.as_ref() {
107110
granularity
108111
.set_group_by_clause(&mut query_builder)

crates/analytics/src/payments/accumulator.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,14 @@ impl PaymentMetricAccumulator for CountAccumulator {
272272
}
273273

274274
impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
275-
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>, Option<u64>);
275+
type MetricOutput = (
276+
Option<u64>,
277+
Option<u64>,
278+
Option<u64>,
279+
Option<u64>,
280+
Option<u64>,
281+
Option<u64>,
282+
);
276283
#[inline]
277284
fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) {
278285
self.total_with_retries = match (
@@ -322,6 +329,8 @@ impl PaymentMetricAccumulator for ProcessedAmountAccumulator {
322329
count_with_retries,
323330
total_without_retries,
324331
count_without_retries,
332+
Some(0),
333+
Some(0),
325334
)
326335
}
327336
}
@@ -378,6 +387,8 @@ impl PaymentMetricsAccumulator {
378387
payment_processed_count,
379388
payment_processed_amount_without_smart_retries,
380389
payment_processed_count_without_smart_retries,
390+
payment_processed_amount_usd,
391+
payment_processed_amount_without_smart_retries_usd,
381392
) = self.processed_amount.collect();
382393
let (
383394
payments_success_rate_distribution,
@@ -406,6 +417,8 @@ impl PaymentMetricsAccumulator {
406417
payments_failure_rate_distribution_without_smart_retries,
407418
failure_reason_count,
408419
failure_reason_count_without_smart_retries,
420+
payment_processed_amount_usd,
421+
payment_processed_amount_without_smart_retries_usd,
409422
}
410423
}
411424
}

0 commit comments

Comments
 (0)