feat(router): add support to use signature_network and is_issuer_regulated as filters (#9033)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Shankar Singh C
2025-08-22 19:16:19 +05:30
committed by GitHub
parent cc44831c51
commit ad05dc4176
27 changed files with 158 additions and 45 deletions

View File

@ -63,8 +63,6 @@ pub struct ProcessedAmountAccumulator {
pub struct DebitRoutingAccumulator {
pub transaction_count: u64,
pub savings_amount: u64,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
}
#[derive(Debug, Default)]
@ -193,13 +191,7 @@ impl PaymentMetricAccumulator for SuccessRateAccumulator {
}
impl PaymentMetricAccumulator for DebitRoutingAccumulator {
type MetricOutput = (
Option<u64>,
Option<u64>,
Option<u64>,
Option<String>,
Option<bool>,
);
type MetricOutput = (Option<u64>, Option<u64>, Option<u64>);
fn add_metrics_bucket(&mut self, metrics: &PaymentMetricRow) {
if let Some(count) = metrics.count {
@ -208,12 +200,6 @@ impl PaymentMetricAccumulator for DebitRoutingAccumulator {
if let Some(total) = metrics.total.as_ref().and_then(ToPrimitive::to_u64) {
self.savings_amount += total;
}
if let Some(signature_network) = &metrics.signature_network {
self.signature_network = Some(signature_network.clone());
}
if let Some(is_issuer_regulated) = metrics.is_issuer_regulated {
self.is_issuer_regulated = Some(is_issuer_regulated);
}
}
fn collect(self) -> Self::MetricOutput {
@ -221,8 +207,6 @@ impl PaymentMetricAccumulator for DebitRoutingAccumulator {
Some(self.transaction_count),
Some(self.savings_amount),
Some(0),
self.signature_network,
self.is_issuer_regulated,
)
}
}
@ -484,13 +468,8 @@ impl PaymentMetricsAccumulator {
) = self.payments_distribution.collect();
let (failure_reason_count, failure_reason_count_without_smart_retries) =
self.failure_reasons_distribution.collect();
let (
debit_routed_transaction_count,
debit_routing_savings,
debit_routing_savings_in_usd,
signature_network,
is_issuer_regulated,
) = self.debit_routing.collect();
let (debit_routed_transaction_count, debit_routing_savings, debit_routing_savings_in_usd) =
self.debit_routing.collect();
PaymentMetricsBucketValue {
payment_success_rate: self.payment_success_rate.collect(),
@ -518,8 +497,6 @@ impl PaymentMetricsAccumulator {
debit_routed_transaction_count,
debit_routing_savings,
debit_routing_savings_in_usd,
signature_network,
is_issuer_regulated,
}
}
}

View File

@ -441,6 +441,9 @@ pub async fn get_filters(
PaymentDimensions::CardIssuer => fil.card_issuer,
PaymentDimensions::ErrorReason => fil.error_reason,
PaymentDimensions::RoutingApproach => fil.routing_approach.map(|i| i.as_ref().to_string()),
PaymentDimensions::SignatureNetwork => fil.signature_network,
PaymentDimensions::IsIssuerRegulated => fil.is_issuer_regulated.map(|b| b.to_string()),
PaymentDimensions::IsDebitRouted => fil.is_debit_routed.map(|b| b.to_string())
})
.collect::<Vec<String>>();
res.query_data.push(FilterValue {

View File

@ -38,6 +38,9 @@ pub struct PaymentDistributionRow {
pub count: Option<i64>,
pub error_message: Option<String>,
pub routing_approach: Option<DBEnumWrapper<storage_enums::RoutingApproach>>,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
pub is_debit_routed: Option<bool>,
#[serde(with = "common_utils::custom_serde::iso8601::option")]
pub start_bucket: Option<PrimitiveDateTime>,
#[serde(with = "common_utils::custom_serde::iso8601::option")]

View File

@ -161,6 +161,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -66,4 +66,7 @@ pub struct PaymentFilterRow {
pub error_reason: Option<String>,
pub first_attempt: Option<bool>,
pub routing_approach: Option<DBEnumWrapper<RoutingApproach>>,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
pub is_debit_routed: Option<bool>,
}

View File

@ -55,6 +55,7 @@ pub struct PaymentMetricRow {
pub routing_approach: Option<DBEnumWrapper<storage_enums::RoutingApproach>>,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
pub is_debit_routed: Option<bool>,
#[serde(with = "common_utils::custom_serde::iso8601::option")]
pub start_bucket: Option<PrimitiveDateTime>,
#[serde(with = "common_utils::custom_serde::iso8601::option")]

View File

@ -123,6 +123,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -118,6 +118,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -57,13 +57,6 @@ where
.switch()?;
query_builder.add_select_column("currency").switch()?;
query_builder
.add_select_column("signature_network")
.switch()?;
query_builder
.add_select_column("is_issuer_regulated")
.switch()?;
query_builder
.add_select_column(Aggregate::Min {
field: "created_at",
@ -93,16 +86,6 @@ where
.switch()?;
}
query_builder
.add_group_by_clause("signature_network")
.attach_printable("Error grouping by signature_network")
.switch()?;
query_builder
.add_group_by_clause("is_issuer_regulated")
.attach_printable("Error grouping by is_issuer_regulated")
.switch()?;
query_builder
.add_group_by_clause("currency")
.attach_printable("Error grouping by currency")
@ -146,6 +129,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -109,6 +109,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -123,6 +123,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -116,6 +116,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -113,6 +113,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -124,6 +124,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -119,6 +119,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -129,6 +129,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -184,6 +184,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -110,6 +110,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -141,6 +141,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -117,6 +117,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -120,6 +120,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -113,6 +113,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -113,6 +113,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -112,6 +112,9 @@ where
i.card_issuer.clone(),
i.error_reason.clone(),
i.routing_approach.as_ref().map(|i| i.0.clone()),
i.signature_network.clone(),
i.is_issuer_regulated,
i.is_debit_routed,
TimeRange {
start_time: match (granularity, i.start_bucket) {
(Some(g), Some(st)) => g.clip_to_start(st)?,

View File

@ -119,6 +119,30 @@ where
.attach_printable("Error adding routing approach filter")?;
}
if !self.signature_network.is_empty() {
builder
.add_filter_in_range_clause(
PaymentDimensions::SignatureNetwork,
&self.signature_network,
)
.attach_printable("Error adding signature network filter")?;
}
if !self.is_issuer_regulated.is_empty() {
builder
.add_filter_in_range_clause(
PaymentDimensions::IsIssuerRegulated,
&self.is_issuer_regulated,
)
.attach_printable("Error adding is issuer regulated filter")?;
}
if !self.is_debit_routed.is_empty() {
builder
.add_filter_in_range_clause(PaymentDimensions::IsDebitRouted, &self.is_debit_routed)
.attach_printable("Error adding is debit routed filter")?;
}
Ok(())
}
}

View File

@ -746,6 +746,11 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let is_debit_routed: Option<bool> =
row.try_get("is_debit_routed").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let total: Option<bigdecimal::BigDecimal> = row.try_get("total").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
@ -780,6 +785,7 @@ impl<'a> FromRow<'a, PgRow> for super::payments::metrics::PaymentMetricRow {
routing_approach,
signature_network,
is_issuer_regulated,
is_debit_routed,
total,
count,
start_bucket,
@ -857,6 +863,22 @@ impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributi
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let signature_network: Option<String> =
row.try_get("signature_network").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let is_issuer_regulated: Option<bool> =
row.try_get("is_issuer_regulated").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let is_debit_routed: Option<bool> =
row.try_get("is_debit_routed").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let total: Option<bigdecimal::BigDecimal> = row.try_get("total").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
@ -900,6 +922,9 @@ impl<'a> FromRow<'a, PgRow> for super::payments::distribution::PaymentDistributi
count,
error_message,
routing_approach,
signature_network,
is_issuer_regulated,
is_debit_routed,
start_bucket,
end_bucket,
})
@ -979,6 +1004,21 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let signature_network: Option<String> =
row.try_get("signature_network").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let is_issuer_regulated: Option<bool> =
row.try_get("is_issuer_regulated").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
let is_debit_routed: Option<bool> =
row.try_get("is_debit_routed").or_else(|e| match e {
ColumnNotFound(_) => Ok(Default::default()),
e => Err(e),
})?;
Ok(Self {
currency,
status,
@ -996,6 +1036,9 @@ impl<'a> FromRow<'a, PgRow> for super::payments::filters::PaymentFilterRow {
error_reason,
first_attempt,
routing_approach,
signature_network,
is_issuer_regulated,
is_debit_routed,
})
}
}

View File

@ -45,6 +45,12 @@ pub struct PaymentFilters {
pub first_attempt: Vec<bool>,
#[serde(default)]
pub routing_approach: Vec<RoutingApproach>,
#[serde(default)]
pub signature_network: Vec<String>,
#[serde(default)]
pub is_issuer_regulated: Vec<bool>,
#[serde(default)]
pub is_debit_routed: Vec<bool>,
}
#[derive(
@ -87,6 +93,9 @@ pub enum PaymentDimensions {
CardIssuer,
ErrorReason,
RoutingApproach,
SignatureNetwork,
IsIssuerRegulated,
IsDebitRouted,
}
#[derive(
@ -208,6 +217,9 @@ pub struct PaymentMetricsBucketIdentifier {
pub card_issuer: Option<String>,
pub error_reason: Option<String>,
pub routing_approach: Option<RoutingApproach>,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
pub is_debit_routed: Option<bool>,
#[serde(rename = "time_range")]
pub time_bucket: TimeRange,
// Coz FE sucks
@ -234,6 +246,9 @@ impl PaymentMetricsBucketIdentifier {
card_issuer: Option<String>,
error_reason: Option<String>,
routing_approach: Option<RoutingApproach>,
signature_network: Option<String>,
is_issuer_regulated: Option<bool>,
is_debit_routed: Option<bool>,
normalized_time_range: TimeRange,
) -> Self {
Self {
@ -252,6 +267,9 @@ impl PaymentMetricsBucketIdentifier {
card_issuer,
error_reason,
routing_approach,
signature_network,
is_issuer_regulated,
is_debit_routed,
time_bucket: normalized_time_range,
start_time: normalized_time_range.start_time,
}
@ -278,6 +296,9 @@ impl Hash for PaymentMetricsBucketIdentifier {
.clone()
.map(|i| i.to_string())
.hash(state);
self.signature_network.hash(state);
self.is_issuer_regulated.hash(state);
self.is_debit_routed.hash(state);
self.time_bucket.hash(state);
}
}
@ -319,8 +340,6 @@ pub struct PaymentMetricsBucketValue {
pub debit_routed_transaction_count: Option<u64>,
pub debit_routing_savings: Option<u64>,
pub debit_routing_savings_in_usd: Option<u64>,
pub signature_network: Option<String>,
pub is_issuer_regulated: Option<bool>,
}
#[derive(Debug, serde::Serialize)]