Skip to content

Commit 3d981b9

Browse files
VenuMadhav2541hyperswitch-bot[bot]
authored andcommitted
feat(webhooks): Adding event search option in the webhooks page (juspay#9907)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 920e2f4 commit 3d981b9

7 files changed

Lines changed: 98 additions & 45 deletions

File tree

api-reference/v1/openapi_spec_v1.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16103,6 +16103,11 @@
1610316103
"description": "Filter all events associated with the specified object identifier (Payment Intent ID,\nRefund ID, etc.)",
1610416104
"nullable": true
1610516105
},
16106+
"event_id": {
16107+
"type": "string",
16108+
"description": "Filter all events associated with the specified Event_id",
16109+
"nullable": true
16110+
},
1610616111
"profile_id": {
1610716112
"type": "string",
1610816113
"description": "Filter all events associated with the specified business profile ID.",

crates/api_models/src/webhook_events.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ pub struct EventListConstraints {
2727
/// Refund ID, etc.)
2828
pub object_id: Option<String>,
2929

30+
/// Filter all events associated with the specified Event_id
31+
pub event_id: Option<String>,
32+
3033
/// Filter all events associated with the specified business profile ID.
3134
#[schema(value_type = Option<String>)]
3235
pub profile_id: Option<common_utils::id_type::ProfileId>,
@@ -53,6 +56,7 @@ pub enum EventListConstraintsInternal {
5356
},
5457
ObjectIdFilter {
5558
object_id: String,
59+
event_id: String,
5660
},
5761
}
5862

crates/diesel_models/src/query/events.rs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,23 @@ impl Event {
4646
.await
4747
}
4848

49-
pub async fn list_initial_attempts_by_merchant_id_primary_object_id(
49+
pub async fn list_initial_attempts_by_merchant_id_primary_object_id_or_initial_attempt_id(
5050
conn: &PgPooledConn,
5151
merchant_id: &common_utils::id_type::MerchantId,
5252
primary_object_id: &str,
53+
initial_attempt_id: &str,
5354
) -> StorageResult<Vec<Self>> {
5455
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
5556
conn,
5657
dsl::event_id
5758
.nullable()
5859
.eq(dsl::initial_attempt_id) // Filter initial attempts only
5960
.and(dsl::merchant_id.eq(merchant_id.to_owned()))
60-
.and(dsl::primary_object_id.eq(primary_object_id.to_owned())),
61+
.and(
62+
dsl::primary_object_id
63+
.eq(primary_object_id.to_owned())
64+
.or(dsl::initial_attempt_id.eq(initial_attempt_id.to_owned())),
65+
),
6166
None,
6267
None,
6368
Some(dsl::created_at.desc()),
@@ -129,18 +134,23 @@ impl Event {
129134
.await
130135
}
131136

132-
pub async fn list_initial_attempts_by_profile_id_primary_object_id(
137+
pub async fn list_initial_attempts_by_profile_id_primary_object_id_or_initial_attempt_id(
133138
conn: &PgPooledConn,
134139
profile_id: &common_utils::id_type::ProfileId,
135140
primary_object_id: &str,
141+
initial_attempt_id: &str,
136142
) -> StorageResult<Vec<Self>> {
137143
generics::generic_filter::<<Self as HasTable>::Table, _, _, _>(
138144
conn,
139145
dsl::event_id
140146
.nullable()
141147
.eq(dsl::initial_attempt_id) // Filter initial attempts only
142148
.and(dsl::business_profile_id.eq(profile_id.to_owned()))
143-
.and(dsl::primary_object_id.eq(primary_object_id.to_owned())),
149+
.and(
150+
dsl::primary_object_id
151+
.eq(primary_object_id.to_owned())
152+
.or(dsl::initial_attempt_id.eq(initial_attempt_id.to_owned())),
153+
),
144154
None,
145155
None,
146156
Some(dsl::created_at.desc()),

crates/router/src/core/webhooks/webhook_events.rs

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -43,31 +43,35 @@ pub async fn list_initial_delivery_attempts(
4343
(now.date() - time::Duration::days(INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_DAYS)).midnight();
4444

4545
let (events, total_count) = match constraints {
46-
api_models::webhook_events::EventListConstraintsInternal::ObjectIdFilter { object_id } => {
47-
let events = match account {
48-
MerchantAccountOrProfile::MerchantAccount(merchant_account) => {
49-
store
50-
.list_initial_events_by_merchant_id_primary_object_id(
46+
api_models::webhook_events::EventListConstraintsInternal::ObjectIdFilter {
47+
object_id,
48+
event_id,
49+
} => {
50+
let events =
51+
match account {
52+
MerchantAccountOrProfile::MerchantAccount(merchant_account) => store
53+
.list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
5154
key_manager_state,
5255
merchant_account.get_id(),
5356
&object_id,
57+
&event_id,
5458
&key_store,
5559
)
56-
.await
57-
}
58-
MerchantAccountOrProfile::Profile(business_profile) => {
59-
store
60-
.list_initial_events_by_profile_id_primary_object_id(
61-
key_manager_state,
62-
business_profile.get_id(),
63-
&object_id,
64-
&key_store,
65-
)
66-
.await
60+
.await,
61+
MerchantAccountOrProfile::Profile(business_profile) => {
62+
store
63+
.list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
64+
key_manager_state,
65+
business_profile.get_id(),
66+
&object_id,
67+
&event_id,
68+
&key_store,
69+
)
70+
.await
71+
}
6772
}
68-
}
69-
.change_context(errors::ApiErrorResponse::InternalServerError)
70-
.attach_printable("Failed to list events with specified constraints")?;
73+
.change_context(errors::ApiErrorResponse::InternalServerError)
74+
.attach_printable("Failed to list events with specified constraints")?;
7175

7276
let total_count = i64::try_from(events.len())
7377
.change_context(errors::ApiErrorResponse::InternalServerError)

crates/router/src/db/events.rs

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ where
4646
merchant_key_store: &domain::MerchantKeyStore,
4747
) -> CustomResult<domain::Event, errors::StorageError>;
4848

49-
async fn list_initial_events_by_merchant_id_primary_object_id(
49+
async fn list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
5050
&self,
5151
state: &KeyManagerState,
5252
merchant_id: &common_utils::id_type::MerchantId,
5353
primary_object_id: &str,
54+
initial_attempt_id: &str,
5455
merchant_key_store: &domain::MerchantKeyStore,
5556
) -> CustomResult<Vec<domain::Event>, errors::StorageError>;
5657

@@ -76,11 +77,12 @@ where
7677
merchant_key_store: &domain::MerchantKeyStore,
7778
) -> CustomResult<Vec<domain::Event>, errors::StorageError>;
7879

79-
async fn list_initial_events_by_profile_id_primary_object_id(
80+
async fn list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
8081
&self,
8182
state: &KeyManagerState,
8283
profile_id: &common_utils::id_type::ProfileId,
8384
primary_object_id: &str,
85+
initial_attempt_id: &str,
8486
merchant_key_store: &domain::MerchantKeyStore,
8587
) -> CustomResult<Vec<domain::Event>, errors::StorageError>;
8688

@@ -191,18 +193,20 @@ impl EventInterface for Store {
191193
}
192194

193195
#[instrument(skip_all)]
194-
async fn list_initial_events_by_merchant_id_primary_object_id(
196+
async fn list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
195197
&self,
196198
state: &KeyManagerState,
197199
merchant_id: &common_utils::id_type::MerchantId,
198200
primary_object_id: &str,
201+
initial_attempt_id: &str,
199202
merchant_key_store: &domain::MerchantKeyStore,
200203
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
201204
let conn = connection::pg_connection_read(self).await?;
202-
storage::Event::list_initial_attempts_by_merchant_id_primary_object_id(
205+
storage::Event::list_initial_attempts_by_merchant_id_primary_object_id_or_initial_attempt_id(
203206
&conn,
204207
merchant_id,
205208
primary_object_id,
209+
initial_attempt_id,
206210
)
207211
.await
208212
.map_err(|error| report!(errors::StorageError::from(error)))
@@ -306,18 +310,20 @@ impl EventInterface for Store {
306310
}
307311

308312
#[instrument(skip_all)]
309-
async fn list_initial_events_by_profile_id_primary_object_id(
313+
async fn list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
310314
&self,
311315
state: &KeyManagerState,
312316
profile_id: &common_utils::id_type::ProfileId,
313317
primary_object_id: &str,
318+
initial_attempt_id: &str,
314319
merchant_key_store: &domain::MerchantKeyStore,
315320
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
316321
let conn = connection::pg_connection_read(self).await?;
317-
storage::Event::list_initial_attempts_by_profile_id_primary_object_id(
322+
storage::Event::list_initial_attempts_by_profile_id_primary_object_id_or_initial_attempt_id(
318323
&conn,
319324
profile_id,
320325
primary_object_id,
326+
initial_attempt_id,
321327
)
322328
.await
323329
.map_err(|error| report!(errors::StorageError::from(error)))
@@ -527,20 +533,22 @@ impl EventInterface for MockDb {
527533
)
528534
}
529535

530-
async fn list_initial_events_by_merchant_id_primary_object_id(
536+
async fn list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
531537
&self,
532538
state: &KeyManagerState,
533539
merchant_id: &common_utils::id_type::MerchantId,
534540
primary_object_id: &str,
541+
initial_attempt_id: &str,
535542
merchant_key_store: &domain::MerchantKeyStore,
536543
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
537544
let locked_events = self.events.lock().await;
538545
let events = locked_events
539546
.iter()
540547
.filter(|event| {
541548
event.merchant_id == Some(merchant_id.to_owned())
542-
&& event.initial_attempt_id.as_ref() == Some(&event.event_id)
543-
&& event.primary_object_id == primary_object_id
549+
&& event.initial_attempt_id.as_deref() == Some(&event.event_id)
550+
&& (event.primary_object_id == primary_object_id
551+
|| event.initial_attempt_id.as_deref() == Some(initial_attempt_id))
544552
})
545553
.cloned()
546554
.collect::<Vec<_>>();
@@ -663,11 +671,12 @@ impl EventInterface for MockDb {
663671
Ok(domain_events)
664672
}
665673

666-
async fn list_initial_events_by_profile_id_primary_object_id(
674+
async fn list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
667675
&self,
668676
state: &KeyManagerState,
669677
profile_id: &common_utils::id_type::ProfileId,
670678
primary_object_id: &str,
679+
initial_attempt_id: &str,
671680
merchant_key_store: &domain::MerchantKeyStore,
672681
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
673682
let locked_events = self.events.lock().await;
@@ -676,7 +685,8 @@ impl EventInterface for MockDb {
676685
.filter(|event| {
677686
event.business_profile_id == Some(profile_id.to_owned())
678687
&& event.initial_attempt_id.as_ref() == Some(&event.event_id)
679-
&& event.primary_object_id == primary_object_id
688+
&& (event.primary_object_id == primary_object_id
689+
|| event.initial_attempt_id.as_deref() == Some(initial_attempt_id))
680690
})
681691
.cloned()
682692
.collect::<Vec<_>>();
@@ -1310,6 +1320,7 @@ mod tests {
13101320
let event_type = enums::EventType::PaymentSucceeded;
13111321
let event_class = enums::EventClass::Payments;
13121322
let primary_object_id = Arc::new("concurrent_payment_id".to_string());
1323+
let initial_attempt_id = Arc::new("initial_attempt_id".to_string());
13131324
let primary_object_type = enums::EventObjectType::PaymentDetails;
13141325
let payment_id = common_utils::id_type::PaymentId::try_from(std::borrow::Cow::Borrowed(
13151326
"pay_mbabizu24mvu3mela5njyhpit10",
@@ -1462,10 +1473,11 @@ mod tests {
14621473
// Collect all initial-attempt events for this payment
14631474
let events = state
14641475
.store
1465-
.list_initial_events_by_merchant_id_primary_object_id(
1476+
.list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
14661477
key_manager_state,
14671478
&business_profile.merchant_id,
14681479
&primary_object_id.clone(),
1480+
&initial_attempt_id.clone(),
14691481
merchant_context.get_merchant_key_store(),
14701482
)
14711483
.await?;

crates/router/src/db/kafka_store.rs

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -782,18 +782,20 @@ impl EventInterface for KafkaStore {
782782
.await
783783
}
784784

785-
async fn list_initial_events_by_merchant_id_primary_object_id(
785+
async fn list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
786786
&self,
787787
state: &KeyManagerState,
788788
merchant_id: &id_type::MerchantId,
789789
primary_object_id: &str,
790+
initial_attempt_id: &str,
790791
merchant_key_store: &domain::MerchantKeyStore,
791792
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
792793
self.diesel_store
793-
.list_initial_events_by_merchant_id_primary_object_id(
794+
.list_initial_events_by_merchant_id_primary_object_or_initial_attempt_id(
794795
state,
795796
merchant_id,
796797
primary_object_id,
798+
initial_attempt_id,
797799
merchant_key_store,
798800
)
799801
.await
@@ -843,18 +845,20 @@ impl EventInterface for KafkaStore {
843845
.await
844846
}
845847

846-
async fn list_initial_events_by_profile_id_primary_object_id(
848+
async fn list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
847849
&self,
848850
state: &KeyManagerState,
849851
profile_id: &id_type::ProfileId,
850852
primary_object_id: &str,
853+
initial_attempt_id: &str,
851854
merchant_key_store: &domain::MerchantKeyStore,
852855
) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
853856
self.diesel_store
854-
.list_initial_events_by_profile_id_primary_object_id(
857+
.list_initial_events_by_profile_id_primary_object_or_initial_attempt_id(
855858
state,
856859
profile_id,
857860
primary_object_id,
861+
initial_attempt_id,
858862
merchant_key_store,
859863
)
860864
.await

crates/router/src/types/transformers.rs

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,7 +1798,7 @@ impl ForeignTryFrom<api_types::webhook_events::EventListConstraints>
17981798
fn foreign_try_from(
17991799
item: api_types::webhook_events::EventListConstraints,
18001800
) -> Result<Self, Self::Error> {
1801-
if item.object_id.is_some()
1801+
if (item.object_id.is_some() || item.event_id.is_some())
18021802
&& (item.created_after.is_some()
18031803
|| item.created_before.is_some()
18041804
|| item.limit.is_some()
@@ -1808,15 +1808,29 @@ impl ForeignTryFrom<api_types::webhook_events::EventListConstraints>
18081808
{
18091809
return Err(report!(errors::ApiErrorResponse::PreconditionFailed {
18101810
message:
1811-
"Either only `object_id` must be specified, or one or more of \
1812-
`created_after`, `created_before`, `limit`, `offset`, `event_classes` and `event_types` must be specified"
1811+
"Either only `object_id` or `event_id` must be specified, or one or more of \
1812+
`created_after`, `created_before`, `limit`, `offset`, `event_classes` and `event_types` must be specified"
18131813
.to_string()
18141814
}));
18151815
}
18161816

1817-
match item.object_id {
1818-
Some(object_id) => Ok(Self::ObjectIdFilter { object_id }),
1819-
None => Ok(Self::GenericFilter {
1817+
match (item.object_id.clone(), item.event_id.clone()) {
1818+
(Some(object_id), Some(event_id)) => Ok(Self::ObjectIdFilter {
1819+
object_id,
1820+
event_id,
1821+
}),
1822+
1823+
(Some(object_id), None) => Ok(Self::ObjectIdFilter {
1824+
event_id: object_id.clone(),
1825+
object_id,
1826+
}),
1827+
1828+
(None, Some(event_id)) => Ok(Self::ObjectIdFilter {
1829+
object_id: event_id.clone(),
1830+
event_id,
1831+
}),
1832+
1833+
(None, None) => Ok(Self::GenericFilter {
18201834
created_after: item.created_after,
18211835
created_before: item.created_before,
18221836
limit: item.limit.map(i64::from),

0 commit comments

Comments
 (0)