Skip to content

Commit 6c2efa3

Browse files
committed
Merge branch 'main' of github.com:juspay/hyperswitch into refactor-rustman-runner
* 'main' of github.com:juspay/hyperswitch: chore(version): 2024.02.29.0 chore(postman): update Postman collection files feat(analytics): add force retrieve call for force retrieve calls (#3565) refactor(connector): [Mollie] Mask PII data (#3856) refactor(connector): [Gocardless] Mask PII data (#3844) feat(analytics): adding metric api for dispute analytics (#3810) feat(payment_methods): Add default payment method column in customers table and last used column in payment_methods table (#3790) fix(tests/postman/adyen): enable sepa payment method type for payout flows (#3861) feat(payouts): Implement Smart Retries for Payout (#3580) refactor(payment_link): add Miscellaneous charges in cart (#3645)
2 parents e761c84 + 6b078fa commit 6c2efa3

File tree

77 files changed

+2382
-212
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

77 files changed

+2382
-212
lines changed

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,35 @@ All notable changes to HyperSwitch will be documented here.
44

55
- - -
66

7+
## 2024.02.29.0
8+
9+
### Features
10+
11+
- **analytics:**
12+
- Adding metric api for dispute analytics ([#3810](https://github.com/juspay/hyperswitch/pull/3810)) ([`de6b16b`](https://github.com/juspay/hyperswitch/commit/de6b16bed98280a4ed8fc8cdad920a759662aa19))
13+
- Add force retrieve call for force retrieve calls ([#3565](https://github.com/juspay/hyperswitch/pull/3565)) ([`032d58c`](https://github.com/juspay/hyperswitch/commit/032d58cdbbf388cf25cbf2e43b0117b83f7d076d))
14+
- **payment_methods:** Add default payment method column in customers table and last used column in payment_methods table ([#3790](https://github.com/juspay/hyperswitch/pull/3790)) ([`f3931cf`](https://github.com/juspay/hyperswitch/commit/f3931cf484f61a4d9c107c362d0f3f6ee872e0e7))
15+
- **payouts:** Implement Smart Retries for Payout ([#3580](https://github.com/juspay/hyperswitch/pull/3580)) ([`8b32dff`](https://github.com/juspay/hyperswitch/commit/8b32dffe324a4cdbfde173cffe3fad2e839a52aa))
16+
17+
### Bug Fixes
18+
19+
- **tests/postman/adyen:** Enable sepa payment method type for payout flows ([#3861](https://github.com/juspay/hyperswitch/pull/3861)) ([`53559c2`](https://github.com/juspay/hyperswitch/commit/53559c22527dde9536aa493ad7cd3bf353335c1a))
20+
21+
### Refactors
22+
23+
- **connector:**
24+
- [Gocardless] Mask PII data ([#3844](https://github.com/juspay/hyperswitch/pull/3844)) ([`2f3ec7f`](https://github.com/juspay/hyperswitch/commit/2f3ec7f951967359d3995f743a486f3b380dd1f8))
25+
- [Mollie] Mask PII data ([#3856](https://github.com/juspay/hyperswitch/pull/3856)) ([`ffbe042`](https://github.com/juspay/hyperswitch/commit/ffbe042fdccde4a721d329d6b85c95203234368e))
26+
- **payment_link:** Add Miscellaneous charges in cart ([#3645](https://github.com/juspay/hyperswitch/pull/3645)) ([`15b367e`](https://github.com/juspay/hyperswitch/commit/15b367eb792448fb3f3312484ab13dd8241d4a14))
27+
28+
### Miscellaneous Tasks
29+
30+
- **postman:** Update Postman collection files ([`5c91a94`](https://github.com/juspay/hyperswitch/commit/5c91a9440e098490cc00a54ead34989da81babc0))
31+
32+
**Full Changelog:** [`2024.02.28.0...2024.02.29.0`](https://github.com/juspay/hyperswitch/compare/2024.02.28.0...2024.02.29.0)
33+
34+
- - -
35+
736
## 2024.02.28.0
837

938
### Features

config/development.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,6 @@ mock_locker = true
7171
basilisk_host = ""
7272
locker_enabled = true
7373

74-
7574
[forex_api]
7675
call_delay = 21600
7776
local_fetch_retry_count = 5

crates/analytics/src/clickhouse.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use crate::{
2323
metrics::{latency::LatencyAvg, ApiEventMetricRow},
2424
},
2525
connector_events::events::ConnectorEventsResult,
26-
disputes::filters::DisputeFilterRow,
26+
disputes::{filters::DisputeFilterRow, metrics::DisputeMetricRow},
2727
outgoing_webhook_event::events::OutgoingWebhookLogsResult,
2828
sdk_events::events::SdkEventsResult,
2929
types::TableEngine,
@@ -170,6 +170,7 @@ impl super::outgoing_webhook_event::events::OutgoingWebhookLogsFilterAnalytics
170170
{
171171
}
172172
impl super::disputes::filters::DisputeFilterAnalytics for ClickhouseClient {}
173+
impl super::disputes::metrics::DisputeMetricAnalytics for ClickhouseClient {}
173174

174175
#[derive(Debug, serde::Serialize)]
175176
struct CkhQuery {
@@ -278,6 +279,17 @@ impl TryInto<RefundFilterRow> for serde_json::Value {
278279
))
279280
}
280281
}
282+
impl TryInto<DisputeMetricRow> for serde_json::Value {
283+
type Error = Report<ParsingError>;
284+
285+
fn try_into(self) -> Result<DisputeMetricRow, Self::Error> {
286+
serde_json::from_value(self)
287+
.into_report()
288+
.change_context(ParsingError::StructParseFailure(
289+
"Failed to parse DisputeMetricRow in clickhouse results",
290+
))
291+
}
292+
}
281293

282294
impl TryInto<DisputeFilterRow> for serde_json::Value {
283295
type Error = Report<ParsingError>;

crates/analytics/src/disputes.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
1+
pub mod accumulators;
12
mod core;
2-
33
pub mod filters;
4+
pub mod metrics;
5+
pub mod types;
6+
pub use accumulators::{DisputeMetricAccumulator, DisputeMetricsAccumulator};
47

5-
pub use self::core::get_filters;
8+
pub trait DisputeAnalytics: metrics::DisputeMetricAnalytics {}
9+
pub use self::core::{get_filters, get_metrics};
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
use api_models::analytics::disputes::DisputeMetricsBucketValue;
2+
use diesel_models::enums as storage_enums;
3+
4+
use super::metrics::DisputeMetricRow;
5+
#[derive(Debug, Default)]
6+
pub struct DisputeMetricsAccumulator {
7+
pub disputes_status_rate: RateAccumulator,
8+
pub total_amount_disputed: SumAccumulator,
9+
pub total_dispute_lost_amount: SumAccumulator,
10+
}
11+
#[derive(Debug, Default)]
12+
pub struct RateAccumulator {
13+
pub won_count: i64,
14+
pub challenged_count: i64,
15+
pub lost_count: i64,
16+
pub total: i64,
17+
}
18+
#[derive(Debug, Default)]
19+
#[repr(transparent)]
20+
pub struct SumAccumulator {
21+
pub total: Option<i64>,
22+
}
23+
24+
pub trait DisputeMetricAccumulator {
25+
type MetricOutput;
26+
27+
fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow);
28+
29+
fn collect(self) -> Self::MetricOutput;
30+
}
31+
32+
impl DisputeMetricAccumulator for SumAccumulator {
33+
type MetricOutput = Option<u64>;
34+
#[inline]
35+
fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) {
36+
self.total = match (
37+
self.total,
38+
metrics
39+
.total
40+
.as_ref()
41+
.and_then(bigdecimal::ToPrimitive::to_i64),
42+
) {
43+
(None, None) => None,
44+
(None, i @ Some(_)) | (i @ Some(_), None) => i,
45+
(Some(a), Some(b)) => Some(a + b),
46+
}
47+
}
48+
#[inline]
49+
fn collect(self) -> Self::MetricOutput {
50+
self.total.and_then(|i| u64::try_from(i).ok())
51+
}
52+
}
53+
54+
impl DisputeMetricAccumulator for RateAccumulator {
55+
type MetricOutput = Option<(Option<u64>, Option<u64>, Option<u64>, Option<u64>)>;
56+
57+
fn add_metrics_bucket(&mut self, metrics: &DisputeMetricRow) {
58+
if let Some(ref dispute_status) = metrics.dispute_status {
59+
if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeChallenged {
60+
self.challenged_count += metrics.count.unwrap_or_default();
61+
}
62+
if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeWon {
63+
self.won_count += metrics.count.unwrap_or_default();
64+
}
65+
if dispute_status.as_ref() == &storage_enums::DisputeStatus::DisputeLost {
66+
self.lost_count += metrics.count.unwrap_or_default();
67+
}
68+
};
69+
70+
self.total += metrics.count.unwrap_or_default();
71+
}
72+
73+
fn collect(self) -> Self::MetricOutput {
74+
if self.total <= 0 {
75+
Some((None, None, None, None))
76+
} else {
77+
Some((
78+
u64::try_from(self.challenged_count).ok(),
79+
u64::try_from(self.won_count).ok(),
80+
u64::try_from(self.lost_count).ok(),
81+
u64::try_from(self.total).ok(),
82+
))
83+
}
84+
}
85+
}
86+
87+
impl DisputeMetricsAccumulator {
88+
pub fn collect(self) -> DisputeMetricsBucketValue {
89+
let (challenge_rate, won_rate, lost_rate, total_dispute) =
90+
self.disputes_status_rate.collect().unwrap_or_default();
91+
DisputeMetricsBucketValue {
92+
disputes_challenged: challenge_rate,
93+
disputes_won: won_rate,
94+
disputes_lost: lost_rate,
95+
total_amount_disputed: self.total_amount_disputed.collect(),
96+
total_dispute_lost_amount: self.total_dispute_lost_amount.collect(),
97+
total_dispute,
98+
}
99+
}
100+
}

crates/analytics/src/disputes/core.rs

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,125 @@
1+
use std::collections::HashMap;
2+
13
use api_models::analytics::{
2-
disputes::DisputeDimensions, DisputeFilterValue, DisputeFiltersResponse,
3-
GetDisputeFilterRequest,
4+
disputes::{
5+
DisputeDimensions, DisputeMetrics, DisputeMetricsBucketIdentifier,
6+
DisputeMetricsBucketResponse,
7+
},
8+
AnalyticsMetadata, DisputeFilterValue, DisputeFiltersResponse, GetDisputeFilterRequest,
9+
GetDisputeMetricRequest, MetricsResponse,
10+
};
11+
use error_stack::{IntoReport, ResultExt};
12+
use router_env::{
13+
logger,
14+
tracing::{self, Instrument},
415
};
5-
use error_stack::ResultExt;
616

7-
use super::filters::{get_dispute_filter_for_dimension, DisputeFilterRow};
17+
use super::{
18+
filters::{get_dispute_filter_for_dimension, DisputeFilterRow},
19+
DisputeMetricsAccumulator,
20+
};
821
use crate::{
22+
disputes::DisputeMetricAccumulator,
923
errors::{AnalyticsError, AnalyticsResult},
10-
AnalyticsProvider,
24+
metrics, AnalyticsProvider,
1125
};
1226

27+
pub async fn get_metrics(
28+
pool: &AnalyticsProvider,
29+
merchant_id: &String,
30+
req: GetDisputeMetricRequest,
31+
) -> AnalyticsResult<MetricsResponse<DisputeMetricsBucketResponse>> {
32+
let mut metrics_accumulator: HashMap<
33+
DisputeMetricsBucketIdentifier,
34+
DisputeMetricsAccumulator,
35+
> = HashMap::new();
36+
let mut set = tokio::task::JoinSet::new();
37+
for metric_type in req.metrics.iter().cloned() {
38+
let req = req.clone();
39+
let pool = pool.clone();
40+
let task_span = tracing::debug_span!(
41+
"analytics_dispute_query",
42+
refund_metric = metric_type.as_ref()
43+
);
44+
// Currently JoinSet works with only static lifetime references even if the task pool does not outlive the given reference
45+
// We can optimize away this clone once that is fixed
46+
let merchant_id_scoped = merchant_id.to_owned();
47+
set.spawn(
48+
async move {
49+
let data = pool
50+
.get_dispute_metrics(
51+
&metric_type,
52+
&req.group_by_names.clone(),
53+
&merchant_id_scoped,
54+
&req.filters,
55+
&req.time_series.map(|t| t.granularity),
56+
&req.time_range,
57+
)
58+
.await
59+
.change_context(AnalyticsError::UnknownError);
60+
(metric_type, data)
61+
}
62+
.instrument(task_span),
63+
);
64+
}
65+
66+
while let Some((metric, data)) = set
67+
.join_next()
68+
.await
69+
.transpose()
70+
.into_report()
71+
.change_context(AnalyticsError::UnknownError)?
72+
{
73+
let data = data?;
74+
let attributes = &[
75+
metrics::request::add_attributes("metric_type", metric.to_string()),
76+
metrics::request::add_attributes("source", pool.to_string()),
77+
];
78+
79+
let value = u64::try_from(data.len());
80+
if let Ok(val) = value {
81+
metrics::BUCKETS_FETCHED.record(&metrics::CONTEXT, val, attributes);
82+
logger::debug!("Attributes: {:?}, Buckets fetched: {}", attributes, val);
83+
}
84+
85+
for (id, value) in data {
86+
logger::debug!(bucket_id=?id, bucket_value=?value, "Bucket row for metric {metric}");
87+
let metrics_builder = metrics_accumulator.entry(id).or_default();
88+
match metric {
89+
DisputeMetrics::DisputeStatusMetric => metrics_builder
90+
.disputes_status_rate
91+
.add_metrics_bucket(&value),
92+
DisputeMetrics::TotalAmountDisputed => metrics_builder
93+
.total_amount_disputed
94+
.add_metrics_bucket(&value),
95+
DisputeMetrics::TotalDisputeLostAmount => metrics_builder
96+
.total_dispute_lost_amount
97+
.add_metrics_bucket(&value),
98+
}
99+
}
100+
101+
logger::debug!(
102+
"Analytics Accumulated Results: metric: {}, results: {:#?}",
103+
metric,
104+
metrics_accumulator
105+
);
106+
}
107+
let query_data: Vec<DisputeMetricsBucketResponse> = metrics_accumulator
108+
.into_iter()
109+
.map(|(id, val)| DisputeMetricsBucketResponse {
110+
values: val.collect(),
111+
dimensions: id,
112+
})
113+
.collect();
114+
115+
Ok(MetricsResponse {
116+
query_data,
117+
meta_data: [AnalyticsMetadata {
118+
current_time_range: req.time_range,
119+
}],
120+
})
121+
}
122+
13123
pub async fn get_filters(
14124
pool: &AnalyticsProvider,
15125
req: GetDisputeFilterRequest,
@@ -76,9 +186,7 @@ pub async fn get_filters(
76186
.change_context(AnalyticsError::UnknownError)?
77187
.into_iter()
78188
.filter_map(|fil: DisputeFilterRow| match dim {
79-
DisputeDimensions::DisputeStatus => fil.dispute_status,
80189
DisputeDimensions::DisputeStage => fil.dispute_stage,
81-
DisputeDimensions::ConnectorStatus => fil.connector_status,
82190
DisputeDimensions::Connector => fil.connector,
83191
})
84192
.collect::<Vec<String>>();

0 commit comments

Comments
 (0)