Skip to content

Commit fc3c64f

Browse files
feat(debit_routing): add debit_routing_savings in analytics payment attempt (#8519)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 26ae469 commit fc3c64f

File tree

26 files changed

+617
-44
lines changed

26 files changed

+617
-44
lines changed

crates/analytics/docs/clickhouse/scripts/payment_attempts.sql

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ CREATE TABLE payment_attempt_queue (
4444
`profile_id` String,
4545
`card_network` Nullable(String),
4646
`routing_approach` LowCardinality(Nullable(String)),
47+
`debit_routing_savings` Nullable(UInt32),
4748
`sign_flag` Int8
4849
) ENGINE = Kafka SETTINGS kafka_broker_list = 'kafka0:29092',
4950
kafka_topic_list = 'hyperswitch-payment-attempt-events',
@@ -98,6 +99,7 @@ CREATE TABLE payment_attempts (
9899
`profile_id` String,
99100
`card_network` Nullable(String),
100101
`routing_approach` LowCardinality(Nullable(String)),
102+
`debit_routing_savings` Nullable(UInt32),
101103
`sign_flag` Int8,
102104
INDEX connectorIndex connector TYPE bloom_filter GRANULARITY 1,
103105
INDEX paymentMethodIndex payment_method TYPE bloom_filter GRANULARITY 1,
@@ -155,6 +157,7 @@ CREATE MATERIALIZED VIEW payment_attempt_mv TO payment_attempts (
155157
`profile_id` String,
156158
`card_network` Nullable(String),
157159
`routing_approach` LowCardinality(Nullable(String)),
160+
`debit_routing_savings` Nullable(UInt32),
158161
`sign_flag` Int8
159162
) AS
160163
SELECT
@@ -204,6 +207,7 @@ SELECT
204207
profile_id,
205208
card_network,
206209
routing_approach,
210+
debit_routing_savings,
207211
sign_flag
208212
FROM
209213
payment_attempt_queue

crates/analytics/src/payments/accumulator.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct PaymentMetricsAccumulator {
1818
pub connector_success_rate: SuccessRateAccumulator,
1919
pub payments_distribution: PaymentsDistributionAccumulator,
2020
pub failure_reasons_distribution: FailureReasonsDistributionAccumulator,
21+
pub debit_routing: DebitRoutingAccumulator,
2122
}
2223

2324
#[derive(Debug, Default)]
@@ -58,6 +59,12 @@ pub struct ProcessedAmountAccumulator {
5859
pub total_without_retries: Option<i64>,
5960
}
6061

62+
#[derive(Debug, Default)]
63+
pub struct DebitRoutingAccumulator {
64+
pub transaction_count: u64,
65+
pub savings_amount: u64,
66+
}
67+
6168
#[derive(Debug, Default)]
6269
pub struct AverageAccumulator {
6370
pub total: u32,
@@ -183,6 +190,27 @@ impl PaymentMetricAccumulator for SuccessRateAccumulator {
183190
}
184191
}
185192

193+
impl PaymentMetricAccumulator for DebitRoutingAccumulator {
194+
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>);
195+
196+
fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) {
197+
if let Some(count) = metrics.count {
198+
self.transaction_count += u64::try_from(count).unwrap_or(0);
199+
}
200+
if let Some(total) = metrics.total.as_ref().and_then(ToPrimitive::to_u64) {
201+
self.savings_amount += total;
202+
}
203+
}
204+
205+
fn collect(self) -> Self::MetricOutput {
206+
(
207+
Some(self.transaction_count),
208+
Some(self.savings_amount),
209+
Some(0),
210+
)
211+
}
212+
}
213+
186214
impl PaymentMetricAccumulator for PaymentsDistributionAccumulator {
187215
type MetricOutput = (
188216
Option<f64>,
@@ -440,6 +468,9 @@ impl PaymentMetricsAccumulator {
440468
) = self.payments_distribution.collect();
441469
let (failure_reason_count, failure_reason_count_without_smart_retries) =
442470
self.failure_reasons_distribution.collect();
471+
let (debit_routed_transaction_count, debit_routing_savings, debit_routing_savings_in_usd) =
472+
self.debit_routing.collect();
473+
443474
PaymentMetricsBucketValue {
444475
payment_success_rate: self.payment_success_rate.collect(),
445476
payment_count: self.payment_count.collect(),
@@ -463,6 +494,9 @@ impl PaymentMetricsAccumulator {
463494
failure_reason_count_without_smart_retries,
464495
payment_processed_amount_in_usd,
465496
payment_processed_amount_without_smart_retries_usd,
497+
debit_routed_transaction_count,
498+
debit_routing_savings,
499+
debit_routing_savings_in_usd,
466500
}
467501
}
468502
}

crates/analytics/src/payments/core.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,9 @@ pub async fn get_metrics(
171171
.connector_success_rate
172172
.add_metrics_bucket(&value);
173173
}
174+
PaymentMetrics::DebitRouting | PaymentMetrics::SessionizedDebitRouting => {
175+
metrics_builder.debit_routing.add_metrics_bucket(&value);
176+
}
174177
PaymentMetrics::PaymentsDistribution => {
175178
metrics_builder
176179
.payments_distribution
@@ -294,6 +297,33 @@ pub async fn get_metrics(
294297
if let Some(count) = collected_values.failure_reason_count_without_smart_retries {
295298
total_failure_reasons_count_without_smart_retries += count;
296299
}
300+
if let Some(savings) = collected_values.debit_routing_savings {
301+
let savings_in_usd = if let Some(ex_rates) = ex_rates {
302+
id.currency
303+
.and_then(|currency| {
304+
i64::try_from(savings)
305+
.inspect_err(|e| {
306+
logger::error!(
307+
"Debit Routing savings conversion error: {:?}",
308+
e
309+
)
310+
})
311+
.ok()
312+
.and_then(|savings_i64| {
313+
convert(ex_rates, currency, Currency::USD, savings_i64)
314+
.inspect_err(|e| {
315+
logger::error!("Currency conversion error: {:?}", e)
316+
})
317+
.ok()
318+
})
319+
})
320+
.map(|savings| (savings * rust_decimal::Decimal::new(100, 0)).to_u64())
321+
.unwrap_or_default()
322+
} else {
323+
None
324+
};
325+
collected_values.debit_routing_savings_in_usd = savings_in_usd;
326+
}
297327
MetricsBucketResponse {
298328
values: collected_values,
299329
dimensions: id,

crates/analytics/src/payments/metrics.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ use crate::{
1515

1616
mod avg_ticket_size;
1717
mod connector_success_rate;
18+
mod debit_routing;
1819
mod payment_count;
1920
mod payment_processed_amount;
2021
mod payment_success_count;
@@ -24,6 +25,7 @@ mod success_rate;
2425

2526
use avg_ticket_size::AvgTicketSize;
2627
use connector_success_rate::ConnectorSuccessRate;
28+
use debit_routing::DebitRouting;
2729
use payment_count::PaymentCount;
2830
use payment_processed_amount::PaymentProcessedAmount;
2931
use payment_success_count::PaymentSuccessCount;
@@ -130,6 +132,11 @@ where
130132
.load_metrics(dimensions, auth, filters, granularity, time_range, pool)
131133
.await
132134
}
135+
Self::DebitRouting => {
136+
DebitRouting
137+
.load_metrics(dimensions, auth, filters, granularity, time_range, pool)
138+
.await
139+
}
133140
Self::SessionizedPaymentSuccessRate => {
134141
sessionized_metrics::PaymentSuccessRate
135142
.load_metrics(dimensions, auth, filters, granularity, time_range, pool)
@@ -175,6 +182,11 @@ where
175182
.load_metrics(dimensions, auth, filters, granularity, time_range, pool)
176183
.await
177184
}
185+
Self::SessionizedDebitRouting => {
186+
sessionized_metrics::DebitRouting
187+
.load_metrics(dimensions, auth, filters, granularity, time_range, pool)
188+
.await
189+
}
178190
}
179191
}
180192
}
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use std::collections::HashSet;
2+
3+
use api_models::analytics::{
4+
payments::{PaymentDimensions, PaymentFilters, PaymentMetricsBucketIdentifier},
5+
Granularity, TimeRange,
6+
};
7+
use common_utils::errors::ReportSwitchExt;
8+
use diesel_models::enums as storage_enums;
9+
use error_stack::ResultExt;
10+
use time::PrimitiveDateTime;
11+
12+
use super::PaymentMetricRow;
13+
use crate::{
14+
enums::AuthInfo,
15+
query::{Aggregate, GroupByClause, QueryBuilder, QueryFilter, SeriesBucket, ToSql, Window},
16+
types::{AnalyticsCollection, AnalyticsDataSource, MetricsError, MetricsResult},
17+
};
18+
19+
#[derive(Default)]
20+
pub(super) struct DebitRouting;
21+
22+
#[async_trait::async_trait]
23+
impl<T> super::PaymentMetric<T> for DebitRouting
24+
where
25+
T: AnalyticsDataSource + super::PaymentMetricAnalytics,
26+
PrimitiveDateTime: ToSql<T>,
27+
AnalyticsCollection: ToSql<T>,
28+
Granularity: GroupByClause<T>,
29+
Aggregate<&'static str>: ToSql<T>,
30+
Window<&'static str>: ToSql<T>,
31+
{
32+
async fn load_metrics(
33+
&self,
34+
dimensions: &[PaymentDimensions],
35+
auth: &AuthInfo,
36+
filters: &PaymentFilters,
37+
granularity: Option<Granularity>,
38+
time_range: &TimeRange,
39+
pool: &T,
40+
) -> MetricsResult<HashSet<(PaymentMetricsBucketIdentifier, PaymentMetricRow)>> {
41+
let mut query_builder: QueryBuilder<T> = QueryBuilder::new(AnalyticsCollection::Payment);
42+
43+
for dim in dimensions.iter() {
44+
query_builder.add_select_column(dim).switch()?;
45+
}
46+
query_builder
47+
.add_select_column(Aggregate::Count {
48+
field: None,
49+
alias: Some("count"),
50+
})
51+
.switch()?;
52+
query_builder
53+
.add_select_column(Aggregate::Sum {
54+
field: "debit_routing_savings",
55+
alias: Some("total"),
56+
})
57+
.switch()?;
58+
query_builder.add_select_column("currency").switch()?;
59+
query_builder
60+
.add_select_column(Aggregate::Min {
61+
field: "created_at",
62+
alias: Some("start_bucket"),
63+
})
64+
.switch()?;
65+
query_builder
66+
.add_select_column(Aggregate::Max {
67+
field: "created_at",
68+
alias: Some("end_bucket"),
69+
})
70+
.switch()?;
71+
72+
filters.set_filter_clause(&mut query_builder).switch()?;
73+
74+
auth.set_filter_clause(&mut query_builder).switch()?;
75+
76+
time_range
77+
.set_filter_clause(&mut query_builder)
78+
.attach_printable("Error filtering time range")
79+
.switch()?;
80+
81+
for dim in dimensions.iter() {
82+
query_builder
83+
.add_group_by_clause(dim)
84+
.attach_printable("Error grouping by dimensions")
85+
.switch()?;
86+
}
87+
88+
query_builder
89+
.add_group_by_clause("currency")
90+
.attach_printable("Error grouping by currency")
91+
.switch()?;
92+
93+
if let Some(granularity) = granularity {
94+
granularity
95+
.set_group_by_clause(&mut query_builder)
96+
.attach_printable("Error adding granularity")
97+
.switch()?;
98+
}
99+
100+
query_builder
101+
.add_filter_clause(
102+
PaymentDimensions::PaymentStatus,
103+
storage_enums::AttemptStatus::Charged,
104+
)
105+
.switch()?;
106+
107+
query_builder
108+
.execute_query::<PaymentMetricRow, _>(pool)
109+
.await
110+
.change_context(MetricsError::QueryBuildingError)?
111+
.change_context(MetricsError::QueryExecutionFailure)?
112+
.into_iter()
113+
.map(|i| {
114+
Ok((
115+
PaymentMetricsBucketIdentifier::new(
116+
i.currency.as_ref().map(|i| i.0),
117+
None,
118+
i.connector.clone(),
119+
i.authentication_type.as_ref().map(|i| i.0),
120+
i.payment_method.clone(),
121+
i.payment_method_type.clone(),
122+
i.client_source.clone(),
123+
i.client_version.clone(),
124+
i.profile_id.clone(),
125+
i.card_network.clone(),
126+
i.merchant_id.clone(),
127+
i.card_last_4.clone(),
128+
i.card_issuer.clone(),
129+
i.error_reason.clone(),
130+
i.routing_approach.as_ref().map(|i| i.0),
131+
TimeRange {
132+
start_time: match (granularity, i.start_bucket) {
133+
(Some(g), Some(st)) => g.clip_to_start(st)?,
134+
_ => time_range.start_time,
135+
},
136+
end_time: granularity.as_ref().map_or_else(
137+
|| Ok(time_range.end_time),
138+
|g| i.end_bucket.map(|et| g.clip_to_end(et)).transpose(),
139+
)?,
140+
},
141+
),
142+
i,
143+
))
144+
})
145+
.collect::<error_stack::Result<
146+
HashSet<(PaymentMetricsBucketIdentifier, PaymentMetricRow)>,
147+
crate::query::PostProcessingError,
148+
>>()
149+
.change_context(MetricsError::PostProcessingFailure)
150+
}
151+
}

crates/analytics/src/payments/metrics/sessionized_metrics.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
mod avg_ticket_size;
22
mod connector_success_rate;
3+
mod debit_routing;
34
mod failure_reasons;
45
mod payment_count;
56
mod payment_processed_amount;
@@ -9,6 +10,7 @@ mod retries_count;
910
mod success_rate;
1011
pub(super) use avg_ticket_size::AvgTicketSize;
1112
pub(super) use connector_success_rate::ConnectorSuccessRate;
13+
pub(super) use debit_routing::DebitRouting;
1214
pub(super) use failure_reasons::FailureReasons;
1315
pub(super) use payment_count::PaymentCount;
1416
pub(super) use payment_processed_amount::PaymentProcessedAmount;

0 commit comments

Comments
 (0)