Skip to content

Commit 4d57f5e

Browse files
feat(webhook): Return events list and total_count on list initial delivery attempt call (#7243)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 80218d0 commit 4d57f5e

File tree

9 files changed

+317
-88
lines changed

9 files changed

+317
-88
lines changed

api-reference/openapi_spec.json

+23-4
Original file line numberDiff line numberDiff line change
@@ -5395,10 +5395,7 @@
53955395
"content": {
53965396
"application/json": {
53975397
"schema": {
5398-
"type": "array",
5399-
"items": {
5400-
"$ref": "#/components/schemas/EventListItemResponse"
5401-
}
5398+
"$ref": "#/components/schemas/TotalEventsResponse"
54025399
}
54035400
}
54045401
}
@@ -26727,6 +26724,28 @@
2672726724
}
2672826725
}
2672926726
},
26727+
"TotalEventsResponse": {
26728+
"type": "object",
26729+
"description": "The response body of list initial delivery attempts api call.",
26730+
"required": [
26731+
"events",
26732+
"total_count"
26733+
],
26734+
"properties": {
26735+
"events": {
26736+
"type": "array",
26737+
"items": {
26738+
"$ref": "#/components/schemas/EventListItemResponse"
26739+
},
26740+
"description": "The list of events"
26741+
},
26742+
"total_count": {
26743+
"type": "integer",
26744+
"format": "int64",
26745+
"description": "Count of total events"
26746+
}
26747+
}
26748+
},
2673026749
"TouchNGoRedirection": {
2673126750
"type": "object"
2673226751
},

crates/api_models/src/webhook_events.rs

+27-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use time::PrimitiveDateTime;
55
use utoipa::ToSchema;
66

77
/// The constraints to apply when filtering events.
8-
#[derive(Debug, Serialize, Deserialize, ToSchema)]
8+
#[derive(Clone, Debug, Serialize, Deserialize, ToSchema)]
99
pub struct EventListConstraints {
1010
/// Filter events created after the specified time.
1111
#[serde(default, with = "common_utils::custom_serde::iso8601::option")]
@@ -82,6 +82,32 @@ pub struct EventListItemResponse {
8282
pub created: PrimitiveDateTime,
8383
}
8484

85+
/// The response body of list initial delivery attempts api call.
86+
#[derive(Debug, Serialize, ToSchema)]
87+
pub struct TotalEventsResponse {
88+
/// The list of events
89+
pub events: Vec<EventListItemResponse>,
90+
/// Count of total events
91+
pub total_count: i64,
92+
}
93+
94+
impl TotalEventsResponse {
95+
pub fn new(total_count: i64, events: Vec<EventListItemResponse>) -> Self {
96+
Self {
97+
events,
98+
total_count,
99+
}
100+
}
101+
}
102+
103+
impl common_utils::events::ApiEventMetric for TotalEventsResponse {
104+
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
105+
Some(common_utils::events::ApiEventsType::Events {
106+
merchant_id: self.events.first().map(|event| event.merchant_id.clone())?,
107+
})
108+
}
109+
}
110+
85111
/// The response body for retrieving an event.
86112
#[derive(Debug, Serialize, ToSchema)]
87113
pub struct EventRetrieveResponse {

crates/diesel_models/src/query/events.rs

+108-34
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ impl Event {
5252
pub async fn list_initial_attempts_by_merchant_id_constraints(
5353
conn: &PgPooledConn,
5454
merchant_id: &common_utils::id_type::MerchantId,
55-
created_after: Option<time::PrimitiveDateTime>,
56-
created_before: Option<time::PrimitiveDateTime>,
55+
created_after: time::PrimitiveDateTime,
56+
created_before: time::PrimitiveDateTime,
5757
limit: Option<i64>,
5858
offset: Option<i64>,
5959
) -> StorageResult<Vec<Self>> {
@@ -75,21 +75,13 @@ impl Event {
7575
.order(dsl::created_at.desc())
7676
.into_boxed();
7777

78-
if let Some(created_after) = created_after {
79-
query = query.filter(dsl::created_at.ge(created_after));
80-
}
81-
82-
if let Some(created_before) = created_before {
83-
query = query.filter(dsl::created_at.le(created_before));
84-
}
85-
86-
if let Some(limit) = limit {
87-
query = query.limit(limit);
88-
}
89-
90-
if let Some(offset) = offset {
91-
query = query.offset(offset);
92-
}
78+
query = Self::apply_filters(
79+
query,
80+
None,
81+
(dsl::created_at, created_after, created_before),
82+
limit,
83+
offset,
84+
);
9385

9486
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());
9587

@@ -138,8 +130,8 @@ impl Event {
138130
pub async fn list_initial_attempts_by_profile_id_constraints(
139131
conn: &PgPooledConn,
140132
profile_id: &common_utils::id_type::ProfileId,
141-
created_after: Option<time::PrimitiveDateTime>,
142-
created_before: Option<time::PrimitiveDateTime>,
133+
created_after: time::PrimitiveDateTime,
134+
created_before: time::PrimitiveDateTime,
143135
limit: Option<i64>,
144136
offset: Option<i64>,
145137
) -> StorageResult<Vec<Self>> {
@@ -161,21 +153,13 @@ impl Event {
161153
.order(dsl::created_at.desc())
162154
.into_boxed();
163155

164-
if let Some(created_after) = created_after {
165-
query = query.filter(dsl::created_at.ge(created_after));
166-
}
167-
168-
if let Some(created_before) = created_before {
169-
query = query.filter(dsl::created_at.le(created_before));
170-
}
171-
172-
if let Some(limit) = limit {
173-
query = query.limit(limit);
174-
}
175-
176-
if let Some(offset) = offset {
177-
query = query.offset(offset);
178-
}
156+
query = Self::apply_filters(
157+
query,
158+
None,
159+
(dsl::created_at, created_after, created_before),
160+
limit,
161+
offset,
162+
);
179163

180164
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());
181165

@@ -222,4 +206,94 @@ impl Event {
222206
)
223207
.await
224208
}
209+
210+
fn apply_filters<T>(
211+
mut query: T,
212+
profile_id: Option<common_utils::id_type::ProfileId>,
213+
(column, created_after, created_before): (
214+
dsl::created_at,
215+
time::PrimitiveDateTime,
216+
time::PrimitiveDateTime,
217+
),
218+
limit: Option<i64>,
219+
offset: Option<i64>,
220+
) -> T
221+
where
222+
T: diesel::query_dsl::methods::LimitDsl<Output = T>
223+
+ diesel::query_dsl::methods::OffsetDsl<Output = T>,
224+
T: diesel::query_dsl::methods::FilterDsl<
225+
diesel::dsl::GtEq<dsl::created_at, time::PrimitiveDateTime>,
226+
Output = T,
227+
>,
228+
T: diesel::query_dsl::methods::FilterDsl<
229+
diesel::dsl::LtEq<dsl::created_at, time::PrimitiveDateTime>,
230+
Output = T,
231+
>,
232+
T: diesel::query_dsl::methods::FilterDsl<
233+
diesel::dsl::Eq<dsl::business_profile_id, common_utils::id_type::ProfileId>,
234+
Output = T,
235+
>,
236+
{
237+
if let Some(profile_id) = profile_id {
238+
query = query.filter(dsl::business_profile_id.eq(profile_id));
239+
}
240+
241+
query = query
242+
.filter(column.ge(created_after))
243+
.filter(column.le(created_before));
244+
245+
if let Some(limit) = limit {
246+
query = query.limit(limit);
247+
}
248+
249+
if let Some(offset) = offset {
250+
query = query.offset(offset);
251+
}
252+
253+
query
254+
}
255+
256+
pub async fn count_initial_attempts_by_constraints(
257+
conn: &PgPooledConn,
258+
merchant_id: &common_utils::id_type::MerchantId,
259+
profile_id: Option<common_utils::id_type::ProfileId>,
260+
created_after: time::PrimitiveDateTime,
261+
created_before: time::PrimitiveDateTime,
262+
) -> StorageResult<i64> {
263+
use async_bb8_diesel::AsyncRunQueryDsl;
264+
use diesel::{debug_query, pg::Pg, QueryDsl};
265+
use error_stack::ResultExt;
266+
use router_env::logger;
267+
268+
use super::generics::db_metrics::{track_database_call, DatabaseOperation};
269+
use crate::errors::DatabaseError;
270+
271+
let mut query = Self::table()
272+
.count()
273+
.filter(
274+
dsl::event_id
275+
.nullable()
276+
.eq(dsl::initial_attempt_id) // Filter initial attempts only
277+
.and(dsl::merchant_id.eq(merchant_id.to_owned())),
278+
)
279+
.into_boxed();
280+
281+
query = Self::apply_filters(
282+
query,
283+
profile_id,
284+
(dsl::created_at, created_after, created_before),
285+
None,
286+
None,
287+
);
288+
289+
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());
290+
291+
track_database_call::<Self, _, _>(
292+
query.get_result_async::<i64>(conn),
293+
DatabaseOperation::Count,
294+
)
295+
.await
296+
.change_context(DatabaseError::Others)
297+
.attach_printable("Error counting events by constraints")
298+
}
225299
}

crates/openapi/src/openapi.rs

+1
Original file line numberDiff line numberDiff line change
@@ -678,6 +678,7 @@ Never share your secret api keys. Keep them guarded and secure.
678678
api_models::webhook_events::EventRetrieveResponse,
679679
api_models::webhook_events::OutgoingWebhookRequestContent,
680680
api_models::webhook_events::OutgoingWebhookResponseContent,
681+
api_models::webhook_events::TotalEventsResponse,
681682
api_models::enums::WebhookDeliveryAttempt,
682683
api_models::enums::PaymentChargeType,
683684
api_models::enums::StripeChargeType,

crates/openapi/src/routes/webhook_events.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
),
4848
),
4949
responses(
50-
(status = 200, description = "List of Events retrieved successfully", body = Vec<EventListItemResponse>),
50+
(status = 200, description = "List of Events retrieved successfully", body = TotalEventsResponse),
5151
),
5252
tag = "Event",
5353
operation_id = "List all Events associated with a Merchant Account or Profile",

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

+62-10
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use common_utils::{self, fp_utils};
12
use error_stack::ResultExt;
23
use masking::PeekInterface;
34
use router_env::{instrument, tracing};
@@ -11,6 +12,7 @@ use crate::{
1112
};
1213

1314
const INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT: i64 = 100;
15+
const INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_DAYS: i64 = 90;
1416

1517
#[derive(Debug)]
1618
enum MerchantAccountOrProfile {
@@ -22,16 +24,21 @@ enum MerchantAccountOrProfile {
2224
pub async fn list_initial_delivery_attempts(
2325
state: SessionState,
2426
merchant_id: common_utils::id_type::MerchantId,
25-
constraints: api::webhook_events::EventListConstraints,
26-
) -> RouterResponse<Vec<api::webhook_events::EventListItemResponse>> {
27-
let profile_id = constraints.profile_id.clone();
28-
let constraints =
29-
api::webhook_events::EventListConstraintsInternal::foreign_try_from(constraints)?;
27+
api_constraints: api::webhook_events::EventListConstraints,
28+
) -> RouterResponse<api::webhook_events::TotalEventsResponse> {
29+
let profile_id = api_constraints.profile_id.clone();
30+
let constraints = api::webhook_events::EventListConstraintsInternal::foreign_try_from(
31+
api_constraints.clone(),
32+
)?;
3033

3134
let store = state.store.as_ref();
3235
let key_manager_state = &(&state).into();
3336
let (account, key_store) =
34-
get_account_and_key_store(state.clone(), merchant_id, profile_id).await?;
37+
get_account_and_key_store(state.clone(), merchant_id.clone(), profile_id.clone()).await?;
38+
39+
let now = common_utils::date_time::now();
40+
let events_list_begin_time =
41+
(now.date() - time::Duration::days(INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_DAYS)).midnight();
3542

3643
let events = match constraints {
3744
api_models::webhook_events::EventListConstraintsInternal::ObjectIdFilter { object_id } => {
@@ -72,6 +79,33 @@ pub async fn list_initial_delivery_attempts(
7279
_ => None,
7380
};
7481

82+
fp_utils::when(!created_after.zip(created_before).map(|(created_after,created_before)| created_after<=created_before).unwrap_or(true), || {
83+
Err(errors::ApiErrorResponse::InvalidRequestData { message: "The `created_after` timestamp must be an earlier timestamp compared to the `created_before` timestamp".to_string() })
84+
})?;
85+
86+
let created_after = match created_after {
87+
Some(created_after) => {
88+
if created_after < events_list_begin_time {
89+
Err(errors::ApiErrorResponse::InvalidRequestData { message: format!("`created_after` must be a timestamp within the past {INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_DAYS} days.") })
90+
}else{
91+
Ok(created_after)
92+
}
93+
},
94+
None => Ok(events_list_begin_time)
95+
}?;
96+
97+
let created_before = match created_before{
98+
Some(created_before) => {
99+
if created_before < events_list_begin_time{
100+
Err(errors::ApiErrorResponse::InvalidRequestData { message: format!("`created_before` must be a timestamp within the past {INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_DAYS} days.") })
101+
}
102+
else{
103+
Ok(created_before)
104+
}
105+
},
106+
None => Ok(now)
107+
}?;
108+
75109
match account {
76110
MerchantAccountOrProfile::MerchantAccount(merchant_account) => store
77111
.list_initial_events_by_merchant_id_constraints(key_manager_state,
@@ -99,11 +133,29 @@ pub async fn list_initial_delivery_attempts(
99133
.change_context(errors::ApiErrorResponse::InternalServerError)
100134
.attach_printable("Failed to list events with specified constraints")?;
101135

136+
let events = events
137+
.into_iter()
138+
.map(api::webhook_events::EventListItemResponse::try_from)
139+
.collect::<Result<Vec<_>, _>>()?;
140+
141+
let created_after = api_constraints
142+
.created_after
143+
.unwrap_or(events_list_begin_time);
144+
let created_before = api_constraints.created_before.unwrap_or(now);
145+
146+
let total_count = store
147+
.count_initial_events_by_constraints(
148+
&merchant_id,
149+
profile_id,
150+
created_after,
151+
created_before,
152+
)
153+
.await
154+
.change_context(errors::ApiErrorResponse::InternalServerError)
155+
.attach_printable("Failed to get total events count")?;
156+
102157
Ok(ApplicationResponse::Json(
103-
events
104-
.into_iter()
105-
.map(api::webhook_events::EventListItemResponse::try_from)
106-
.collect::<Result<Vec<_>, _>>()?,
158+
api::webhook_events::TotalEventsResponse::new(total_count, events),
107159
))
108160
}
109161

0 commit comments

Comments
 (0)