feat(webhook): Add is_delivered filter to list initial attempts endpoint (#7344)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Amey Wale
2025-03-28 15:59:31 +05:30
committed by GitHub
parent 4b39dc85d7
commit 55d27ce15f
17 changed files with 194 additions and 12 deletions

View File

@ -9566,7 +9566,6 @@
"object_id", "object_id",
"event_type", "event_type",
"event_class", "event_class",
"is_delivery_successful",
"initial_attempt_id", "initial_attempt_id",
"created" "created"
], ],
@ -9603,7 +9602,8 @@
}, },
"is_delivery_successful": { "is_delivery_successful": {
"type": "boolean", "type": "boolean",
"description": "Indicates whether the webhook delivery attempt was successful." "description": "Indicates whether the webhook was ultimately delivered or not.",
"nullable": true
}, },
"initial_attempt_id": { "initial_attempt_id": {
"type": "string", "type": "string",

View File

@ -5387,6 +5387,16 @@
"type": "string", "type": "string",
"nullable": true "nullable": true
} }
},
{
"name": "is_delivered",
"in": "query",
"description": "Only include Events which are ultimately delivered to the merchant.",
"required": false,
"schema": {
"type": "boolean",
"nullable": true
}
} }
], ],
"responses": { "responses": {
@ -11751,7 +11761,6 @@
"object_id", "object_id",
"event_type", "event_type",
"event_class", "event_class",
"is_delivery_successful",
"initial_attempt_id", "initial_attempt_id",
"created" "created"
], ],
@ -11788,7 +11797,8 @@
}, },
"is_delivery_successful": { "is_delivery_successful": {
"type": "boolean", "type": "boolean",
"description": "Indicates whether the webhook delivery attempt was successful." "description": "Indicates whether the webhook was ultimately delivered or not.",
"nullable": true
}, },
"initial_attempt_id": { "initial_attempt_id": {
"type": "string", "type": "string",

View File

@ -28,6 +28,9 @@ pub struct EventListConstraints {
/// Filter all events associated with the specified business profile ID. /// Filter all events associated with the specified business profile ID.
#[schema(value_type = Option<String>)] #[schema(value_type = Option<String>)]
pub profile_id: Option<common_utils::id_type::ProfileId>, pub profile_id: Option<common_utils::id_type::ProfileId>,
/// Filter all events by `is_overall_delivery_successful` field of the event.
pub is_delivered: Option<bool>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -37,6 +40,7 @@ pub enum EventListConstraintsInternal {
created_before: Option<PrimitiveDateTime>, created_before: Option<PrimitiveDateTime>,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
}, },
ObjectIdFilter { ObjectIdFilter {
object_id: String, object_id: String,
@ -68,8 +72,8 @@ pub struct EventListItemResponse {
/// Specifies the class of event (the type of object: Payment, Refund, etc.) /// Specifies the class of event (the type of object: Payment, Refund, etc.)
pub event_class: EventClass, pub event_class: EventClass,
/// Indicates whether the webhook delivery attempt was successful. /// Indicates whether the webhook was ultimately delivered or not.
pub is_delivery_successful: bool, pub is_delivery_successful: Option<bool>,
/// The identifier for the initial delivery attempt. This will be the same as `event_id` for /// The identifier for the initial delivery attempt. This will be the same as `event_id` for
/// the initial delivery attempt. /// the initial delivery attempt.

View File

@ -26,6 +26,7 @@ pub struct EventNew {
pub response: Option<Encryption>, pub response: Option<Encryption>,
pub delivery_attempt: Option<storage_enums::WebhookDeliveryAttempt>, pub delivery_attempt: Option<storage_enums::WebhookDeliveryAttempt>,
pub metadata: Option<EventMetadata>, pub metadata: Option<EventMetadata>,
pub is_overall_delivery_successful: Option<bool>,
} }
#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
@ -33,6 +34,7 @@ pub struct EventNew {
pub struct EventUpdateInternal { pub struct EventUpdateInternal {
pub is_webhook_notified: Option<bool>, pub is_webhook_notified: Option<bool>,
pub response: Option<Encryption>, pub response: Option<Encryption>,
pub is_overall_delivery_successful: Option<bool>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, Identifiable, Queryable, Selectable)] #[derive(Clone, Debug, Deserialize, Serialize, Identifiable, Queryable, Selectable)]
@ -57,6 +59,7 @@ pub struct Event {
pub response: Option<Encryption>, pub response: Option<Encryption>,
pub delivery_attempt: Option<storage_enums::WebhookDeliveryAttempt>, pub delivery_attempt: Option<storage_enums::WebhookDeliveryAttempt>,
pub metadata: Option<EventMetadata>, pub metadata: Option<EventMetadata>,
pub is_overall_delivery_successful: Option<bool>,
} }
#[derive(Clone, Debug, Deserialize, Serialize, AsExpression, diesel::FromSqlRow)] #[derive(Clone, Debug, Deserialize, Serialize, AsExpression, diesel::FromSqlRow)]

View File

@ -56,6 +56,7 @@ impl Event {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
) -> StorageResult<Vec<Self>> { ) -> StorageResult<Vec<Self>> {
use async_bb8_diesel::AsyncRunQueryDsl; use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{debug_query, pg::Pg, QueryDsl}; use diesel::{debug_query, pg::Pg, QueryDsl};
@ -81,6 +82,7 @@ impl Event {
(dsl::created_at, created_after, created_before), (dsl::created_at, created_after, created_before),
limit, limit,
offset, offset,
is_delivered,
); );
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string()); logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());
@ -134,6 +136,7 @@ impl Event {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
) -> StorageResult<Vec<Self>> { ) -> StorageResult<Vec<Self>> {
use async_bb8_diesel::AsyncRunQueryDsl; use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{debug_query, pg::Pg, QueryDsl}; use diesel::{debug_query, pg::Pg, QueryDsl};
@ -159,6 +162,7 @@ impl Event {
(dsl::created_at, created_after, created_before), (dsl::created_at, created_after, created_before),
limit, limit,
offset, offset,
is_delivered,
); );
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string()); logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());
@ -217,6 +221,7 @@ impl Event {
), ),
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
) -> T ) -> T
where where
T: diesel::query_dsl::methods::LimitDsl<Output = T> T: diesel::query_dsl::methods::LimitDsl<Output = T>
@ -233,6 +238,10 @@ impl Event {
diesel::dsl::Eq<dsl::business_profile_id, common_utils::id_type::ProfileId>, diesel::dsl::Eq<dsl::business_profile_id, common_utils::id_type::ProfileId>,
Output = T, Output = T,
>, >,
T: diesel::query_dsl::methods::FilterDsl<
diesel::dsl::Eq<dsl::is_overall_delivery_successful, bool>,
Output = T,
>,
{ {
if let Some(profile_id) = profile_id { if let Some(profile_id) = profile_id {
query = query.filter(dsl::business_profile_id.eq(profile_id)); query = query.filter(dsl::business_profile_id.eq(profile_id));
@ -250,6 +259,10 @@ impl Event {
query = query.offset(offset); query = query.offset(offset);
} }
if let Some(is_delivered) = is_delivered {
query = query.filter(dsl::is_overall_delivery_successful.eq(is_delivered));
}
query query
} }
@ -259,6 +272,7 @@ impl Event {
profile_id: Option<common_utils::id_type::ProfileId>, profile_id: Option<common_utils::id_type::ProfileId>,
created_after: time::PrimitiveDateTime, created_after: time::PrimitiveDateTime,
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
is_delivered: Option<bool>,
) -> StorageResult<i64> { ) -> StorageResult<i64> {
use async_bb8_diesel::AsyncRunQueryDsl; use async_bb8_diesel::AsyncRunQueryDsl;
use diesel::{debug_query, pg::Pg, QueryDsl}; use diesel::{debug_query, pg::Pg, QueryDsl};
@ -284,6 +298,7 @@ impl Event {
(dsl::created_at, created_after, created_before), (dsl::created_at, created_after, created_before),
None, None,
None, None,
is_delivered,
); );
logger::debug!(query = %debug_query::<Pg, _>(&query).to_string()); logger::debug!(query = %debug_query::<Pg, _>(&query).to_string());

View File

@ -474,6 +474,7 @@ diesel::table! {
response -> Nullable<Bytea>, response -> Nullable<Bytea>,
delivery_attempt -> Nullable<WebhookDeliveryAttempt>, delivery_attempt -> Nullable<WebhookDeliveryAttempt>,
metadata -> Nullable<Jsonb>, metadata -> Nullable<Jsonb>,
is_overall_delivery_successful -> Nullable<Bool>,
} }
} }

View File

@ -485,6 +485,7 @@ diesel::table! {
response -> Nullable<Bytea>, response -> Nullable<Bytea>,
delivery_attempt -> Nullable<WebhookDeliveryAttempt>, delivery_attempt -> Nullable<WebhookDeliveryAttempt>,
metadata -> Nullable<Jsonb>, metadata -> Nullable<Jsonb>,
is_overall_delivery_successful -> Nullable<Bool>,
} }
} }

View File

@ -45,6 +45,11 @@
Query, Query,
description = "Only include Events associated with the Profile identified by the specified Profile ID." description = "Only include Events associated with the Profile identified by the specified Profile ID."
), ),
(
"is_delivered" = Option<bool>,
Query,
description = "Only include Events which are ultimately delivered to the merchant."
),
), ),
responses( responses(
(status = 200, description = "List of Events retrieved successfully", body = TotalEventsResponse), (status = 200, description = "List of Events retrieved successfully", body = TotalEventsResponse),

View File

@ -133,6 +133,7 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
response: None, response: None,
delivery_attempt: Some(delivery_attempt), delivery_attempt: Some(delivery_attempt),
metadata: Some(event_metadata), metadata: Some(event_metadata),
is_overall_delivery_successful: Some(false),
}; };
let event_insert_result = state let event_insert_result = state
@ -312,7 +313,7 @@ async fn trigger_webhook_to_merchant(
} }
Ok(response) => { Ok(response) => {
let status_code = response.status(); let status_code = response.status();
let _updated_event = update_event_in_storage( let updated_event = update_event_in_storage(
state.clone(), state.clone(),
merchant_key_store.clone(), merchant_key_store.clone(),
&business_profile.merchant_id, &business_profile.merchant_id,
@ -322,6 +323,14 @@ async fn trigger_webhook_to_merchant(
.await?; .await?;
if status_code.is_success() { if status_code.is_success() {
update_overall_delivery_status_in_storage(
state.clone(),
merchant_key_store.clone(),
&business_profile.merchant_id,
updated_event,
)
.await?;
success_response_handler( success_response_handler(
state.clone(), state.clone(),
&business_profile.merchant_id, &business_profile.merchant_id,
@ -362,7 +371,7 @@ async fn trigger_webhook_to_merchant(
} }
Ok(response) => { Ok(response) => {
let status_code = response.status(); let status_code = response.status();
let _updated_event = update_event_in_storage( let updated_event = update_event_in_storage(
state.clone(), state.clone(),
merchant_key_store.clone(), merchant_key_store.clone(),
&business_profile.merchant_id, &business_profile.merchant_id,
@ -372,6 +381,14 @@ async fn trigger_webhook_to_merchant(
.await?; .await?;
if status_code.is_success() { if status_code.is_success() {
update_overall_delivery_status_in_storage(
state.clone(),
merchant_key_store.clone(),
&business_profile.merchant_id,
updated_event,
)
.await?;
success_response_handler( success_response_handler(
state.clone(), state.clone(),
&business_profile.merchant_id, &business_profile.merchant_id,
@ -837,6 +854,44 @@ async fn update_event_in_storage(
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed) .change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
} }
async fn update_overall_delivery_status_in_storage(
state: SessionState,
merchant_key_store: domain::MerchantKeyStore,
merchant_id: &common_utils::id_type::MerchantId,
updated_event: domain::Event,
) -> CustomResult<(), errors::WebhooksFlowError> {
let key_manager_state = &(&state).into();
let update_overall_delivery_status = domain::EventUpdate::OverallDeliveryStatusUpdate {
is_overall_delivery_successful: true,
};
let initial_attempt_id = updated_event.initial_attempt_id.as_ref();
let delivery_attempt = updated_event.delivery_attempt;
if let Some((
initial_attempt_id,
enums::WebhookDeliveryAttempt::InitialAttempt
| enums::WebhookDeliveryAttempt::AutomaticRetry,
)) = initial_attempt_id.zip(delivery_attempt)
{
state
.store
.update_event_by_merchant_id_event_id(
key_manager_state,
merchant_id,
initial_attempt_id.as_str(),
update_overall_delivery_status,
&merchant_key_store,
)
.await
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
.attach_printable("Failed to update initial delivery attempt")?;
}
Ok(())
}
fn increment_webhook_outgoing_received_count(merchant_id: &common_utils::id_type::MerchantId) { fn increment_webhook_outgoing_received_count(merchant_id: &common_utils::id_type::MerchantId) {
metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add( metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
1, 1,

View File

@ -64,6 +64,7 @@ pub async fn list_initial_delivery_attempts(
created_before, created_before,
limit, limit,
offset, offset,
is_delivered
} => { } => {
let limit = match limit { let limit = match limit {
Some(limit) if limit <= INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT => Ok(Some(limit)), Some(limit) if limit <= INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT => Ok(Some(limit)),
@ -114,6 +115,7 @@ pub async fn list_initial_delivery_attempts(
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
&key_store, &key_store,
) )
.await, .await,
@ -124,6 +126,7 @@ pub async fn list_initial_delivery_attempts(
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
&key_store, &key_store,
) )
.await, .await,
@ -143,12 +146,15 @@ pub async fn list_initial_delivery_attempts(
.unwrap_or(events_list_begin_time); .unwrap_or(events_list_begin_time);
let created_before = api_constraints.created_before.unwrap_or(now); let created_before = api_constraints.created_before.unwrap_or(now);
let is_delivered = api_constraints.is_delivered;
let total_count = store let total_count = store
.count_initial_events_by_constraints( .count_initial_events_by_constraints(
&merchant_id, &merchant_id,
profile_id, profile_id,
created_after, created_after,
created_before, created_before,
is_delivered,
) )
.await .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
@ -269,6 +275,7 @@ pub async fn retry_delivery_attempt(
response: None, response: None,
delivery_attempt: Some(delivery_attempt), delivery_attempt: Some(delivery_attempt),
metadata: event_to_retry.metadata, metadata: event_to_retry.metadata,
is_overall_delivery_successful: Some(false),
}; };
let event = store let event = store

View File

@ -53,6 +53,7 @@ where
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError>; ) -> CustomResult<Vec<domain::Event>, errors::StorageError>;
@ -81,6 +82,7 @@ where
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError>; ) -> CustomResult<Vec<domain::Event>, errors::StorageError>;
@ -99,6 +101,7 @@ where
profile_id: Option<common_utils::id_type::ProfileId>, profile_id: Option<common_utils::id_type::ProfileId>,
created_after: time::PrimitiveDateTime, created_after: time::PrimitiveDateTime,
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
is_delivered: Option<bool>,
) -> CustomResult<i64, errors::StorageError>; ) -> CustomResult<i64, errors::StorageError>;
} }
@ -193,6 +196,7 @@ impl EventInterface for Store {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?; let conn = connection::pg_connection_read(self).await?;
@ -203,6 +207,7 @@ impl EventInterface for Store {
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
) )
.await .await
.map_err(|error| report!(errors::StorageError::from(error))) .map_err(|error| report!(errors::StorageError::from(error)))
@ -304,6 +309,7 @@ impl EventInterface for Store {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?; let conn = connection::pg_connection_read(self).await?;
@ -314,6 +320,7 @@ impl EventInterface for Store {
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
) )
.await .await
.map_err(|error| report!(errors::StorageError::from(error))) .map_err(|error| report!(errors::StorageError::from(error)))
@ -366,6 +373,7 @@ impl EventInterface for Store {
profile_id: Option<common_utils::id_type::ProfileId>, profile_id: Option<common_utils::id_type::ProfileId>,
created_after: time::PrimitiveDateTime, created_after: time::PrimitiveDateTime,
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
is_delivered: Option<bool>,
) -> CustomResult<i64, errors::StorageError> { ) -> CustomResult<i64, errors::StorageError> {
let conn = connection::pg_connection_read(self).await?; let conn = connection::pg_connection_read(self).await?;
storage::Event::count_initial_attempts_by_constraints( storage::Event::count_initial_attempts_by_constraints(
@ -374,6 +382,7 @@ impl EventInterface for Store {
profile_id, profile_id,
created_after, created_after,
created_before, created_before,
is_delivered,
) )
.await .await
.map_err(|error| report!(errors::StorageError::from(error))) .map_err(|error| report!(errors::StorageError::from(error)))
@ -483,6 +492,7 @@ impl EventInterface for MockDb {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
let locked_events = self.events.lock().await; let locked_events = self.events.lock().await;
@ -490,10 +500,12 @@ impl EventInterface for MockDb {
let check = event.merchant_id == Some(merchant_id.to_owned()) let check = event.merchant_id == Some(merchant_id.to_owned())
&& event.initial_attempt_id.as_ref() == Some(&event.event_id) && event.initial_attempt_id.as_ref() == Some(&event.event_id)
&& (event.created_at >= created_after) && (event.created_at >= created_after)
&& (event.created_at <= created_before); && (event.created_at <= created_before)
&& (event.is_overall_delivery_successful == is_delivered);
check check
}); });
let offset: usize = if let Some(offset) = offset { let offset: usize = if let Some(offset) = offset {
if offset < 0 { if offset < 0 {
Err(errors::StorageError::MockDbError)?; Err(errors::StorageError::MockDbError)?;
@ -614,6 +626,7 @@ impl EventInterface for MockDb {
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
let locked_events = self.events.lock().await; let locked_events = self.events.lock().await;
@ -621,7 +634,8 @@ impl EventInterface for MockDb {
let check = event.business_profile_id == Some(profile_id.to_owned()) let check = event.business_profile_id == Some(profile_id.to_owned())
&& event.initial_attempt_id.as_ref() == Some(&event.event_id) && event.initial_attempt_id.as_ref() == Some(&event.event_id)
&& (event.created_at >= created_after) && (event.created_at >= created_after)
&& (event.created_at <= created_before); && (event.created_at <= created_before)
&& (event.is_overall_delivery_successful == is_delivered);
check check
}); });
@ -694,6 +708,12 @@ impl EventInterface for MockDb {
event_to_update.is_webhook_notified = is_webhook_notified; event_to_update.is_webhook_notified = is_webhook_notified;
event_to_update.response = response.map(Into::into); event_to_update.response = response.map(Into::into);
} }
domain::EventUpdate::OverallDeliveryStatusUpdate {
is_overall_delivery_successful,
} => {
event_to_update.is_overall_delivery_successful =
Some(is_overall_delivery_successful)
}
} }
event_to_update event_to_update
@ -713,6 +733,7 @@ impl EventInterface for MockDb {
profile_id: Option<common_utils::id_type::ProfileId>, profile_id: Option<common_utils::id_type::ProfileId>,
created_after: time::PrimitiveDateTime, created_after: time::PrimitiveDateTime,
created_before: time::PrimitiveDateTime, created_before: time::PrimitiveDateTime,
is_delivered: Option<bool>,
) -> CustomResult<i64, errors::StorageError> { ) -> CustomResult<i64, errors::StorageError> {
let locked_events = self.events.lock().await; let locked_events = self.events.lock().await;
@ -721,7 +742,8 @@ impl EventInterface for MockDb {
&& (event.merchant_id == Some(merchant_id.to_owned())) && (event.merchant_id == Some(merchant_id.to_owned()))
&& (event.business_profile_id == profile_id) && (event.business_profile_id == profile_id)
&& (event.created_at >= created_after) && (event.created_at >= created_after)
&& (event.created_at <= created_before); && (event.created_at <= created_before)
&& (event.is_overall_delivery_successful == is_delivered);
check check
}); });
@ -843,6 +865,7 @@ mod tests {
) )
.unwrap(), .unwrap(),
}), }),
is_overall_delivery_successful: Some(false),
}, },
&merchant_key_store, &merchant_key_store,
) )

View File

@ -772,6 +772,7 @@ impl EventInterface for KafkaStore {
created_before: PrimitiveDateTime, created_before: PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
self.diesel_store self.diesel_store
@ -782,6 +783,7 @@ impl EventInterface for KafkaStore {
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
merchant_key_store, merchant_key_store,
) )
.await .await
@ -829,6 +831,7 @@ impl EventInterface for KafkaStore {
created_before: PrimitiveDateTime, created_before: PrimitiveDateTime,
limit: Option<i64>, limit: Option<i64>,
offset: Option<i64>, offset: Option<i64>,
is_delivered: Option<bool>,
merchant_key_store: &domain::MerchantKeyStore, merchant_key_store: &domain::MerchantKeyStore,
) -> CustomResult<Vec<domain::Event>, errors::StorageError> { ) -> CustomResult<Vec<domain::Event>, errors::StorageError> {
self.diesel_store self.diesel_store
@ -839,6 +842,7 @@ impl EventInterface for KafkaStore {
created_before, created_before,
limit, limit,
offset, offset,
is_delivered,
merchant_key_store, merchant_key_store,
) )
.await .await
@ -869,6 +873,7 @@ impl EventInterface for KafkaStore {
profile_id: Option<id_type::ProfileId>, profile_id: Option<id_type::ProfileId>,
created_after: PrimitiveDateTime, created_after: PrimitiveDateTime,
created_before: PrimitiveDateTime, created_before: PrimitiveDateTime,
is_delivered: Option<bool>,
) -> CustomResult<i64, errors::StorageError> { ) -> CustomResult<i64, errors::StorageError> {
self.diesel_store self.diesel_store
.count_initial_events_by_constraints( .count_initial_events_by_constraints(
@ -876,6 +881,7 @@ impl EventInterface for KafkaStore {
profile_id, profile_id,
created_after, created_after,
created_before, created_before,
is_delivered,
) )
.await .await
} }

View File

@ -19,24 +19,58 @@ use crate::{
#[derive(Clone, Debug, router_derive::ToEncryption)] #[derive(Clone, Debug, router_derive::ToEncryption)]
pub struct Event { pub struct Event {
/// A string that uniquely identifies the event.
pub event_id: String, pub event_id: String,
/// Represents the type of event for the webhook.
pub event_type: EventType, pub event_type: EventType,
/// Represents the class of event for the webhook.
pub event_class: EventClass, pub event_class: EventClass,
/// Indicates whether the current webhook delivery was successful.
pub is_webhook_notified: bool, pub is_webhook_notified: bool,
/// Reference to the object for which the webhook was created.
pub primary_object_id: String, pub primary_object_id: String,
/// Type of the object type for which the webhook was created.
pub primary_object_type: EventObjectType, pub primary_object_type: EventObjectType,
/// The timestamp when the webhook was created.
pub created_at: time::PrimitiveDateTime, pub created_at: time::PrimitiveDateTime,
/// Merchant Account identifier to which the object is associated with.
pub merchant_id: Option<common_utils::id_type::MerchantId>, pub merchant_id: Option<common_utils::id_type::MerchantId>,
/// Business Profile identifier to which the object is associated with.
pub business_profile_id: Option<common_utils::id_type::ProfileId>, pub business_profile_id: Option<common_utils::id_type::ProfileId>,
/// The timestamp when the primary object was created.
pub primary_object_created_at: Option<time::PrimitiveDateTime>, pub primary_object_created_at: Option<time::PrimitiveDateTime>,
/// This allows the event to be uniquely identified to prevent multiple processing.
pub idempotent_event_id: Option<String>, pub idempotent_event_id: Option<String>,
/// Links to the initial attempt of the event.
pub initial_attempt_id: Option<String>, pub initial_attempt_id: Option<String>,
/// This field contains the encrypted request data sent as part of the event.
#[encrypt] #[encrypt]
pub request: Option<Encryptable<Secret<String>>>, pub request: Option<Encryptable<Secret<String>>>,
/// This field contains the encrypted response data received as part of the event.
#[encrypt] #[encrypt]
pub response: Option<Encryptable<Secret<String>>>, pub response: Option<Encryptable<Secret<String>>>,
/// Represents the event delivery type.
pub delivery_attempt: Option<WebhookDeliveryAttempt>, pub delivery_attempt: Option<WebhookDeliveryAttempt>,
/// Holds any additional data related to the event.
pub metadata: Option<EventMetadata>, pub metadata: Option<EventMetadata>,
/// Indicates whether the event was ultimately delivered.
pub is_overall_delivery_successful: Option<bool>,
} }
#[derive(Debug)] #[derive(Debug)]
@ -45,6 +79,9 @@ pub enum EventUpdate {
is_webhook_notified: bool, is_webhook_notified: bool,
response: OptionalEncryptableSecretString, response: OptionalEncryptableSecretString,
}, },
OverallDeliveryStatusUpdate {
is_overall_delivery_successful: bool,
},
} }
impl From<EventUpdate> for EventUpdateInternal { impl From<EventUpdate> for EventUpdateInternal {
@ -56,6 +93,14 @@ impl From<EventUpdate> for EventUpdateInternal {
} => Self { } => Self {
is_webhook_notified: Some(is_webhook_notified), is_webhook_notified: Some(is_webhook_notified),
response: response.map(Into::into), response: response.map(Into::into),
is_overall_delivery_successful: None,
},
EventUpdate::OverallDeliveryStatusUpdate {
is_overall_delivery_successful,
} => Self {
is_webhook_notified: None,
response: None,
is_overall_delivery_successful: Some(is_overall_delivery_successful),
}, },
} }
} }
@ -84,6 +129,7 @@ impl super::behaviour::Conversion for Event {
response: self.response.map(Into::into), response: self.response.map(Into::into),
delivery_attempt: self.delivery_attempt, delivery_attempt: self.delivery_attempt,
metadata: self.metadata, metadata: self.metadata,
is_overall_delivery_successful: self.is_overall_delivery_successful,
}) })
} }
@ -133,6 +179,7 @@ impl super::behaviour::Conversion for Event {
response: encryptable_event.response, response: encryptable_event.response,
delivery_attempt: item.delivery_attempt, delivery_attempt: item.delivery_attempt,
metadata: item.metadata, metadata: item.metadata,
is_overall_delivery_successful: item.is_overall_delivery_successful,
}) })
} }
@ -154,6 +201,7 @@ impl super::behaviour::Conversion for Event {
response: self.response.map(Into::into), response: self.response.map(Into::into),
delivery_attempt: self.delivery_attempt, delivery_attempt: self.delivery_attempt,
metadata: self.metadata, metadata: self.metadata,
is_overall_delivery_successful: self.is_overall_delivery_successful,
}) })
} }
} }

View File

@ -1936,6 +1936,7 @@ impl ForeignTryFrom<api_types::webhook_events::EventListConstraints>
created_before: item.created_before, created_before: item.created_before,
limit: item.limit.map(i64::from), limit: item.limit.map(i64::from),
offset: item.offset.map(i64::from), offset: item.offset.map(i64::from),
is_delivered: item.is_delivered,
}), }),
} }
} }
@ -1971,7 +1972,7 @@ impl TryFrom<domain::Event> for api_models::webhook_events::EventListItemRespons
object_id: item.primary_object_id, object_id: item.primary_object_id,
event_type: item.event_type, event_type: item.event_type,
event_class: item.event_class, event_class: item.event_class,
is_delivery_successful: item.is_webhook_notified, is_delivery_successful: item.is_overall_delivery_successful,
initial_attempt_id, initial_attempt_id,
created: item.created_at, created: item.created_at,
}) })

View File

@ -118,6 +118,7 @@ impl ProcessTrackerWorkflow<SessionState> for OutgoingWebhookRetryWorkflow {
response: None, response: None,
delivery_attempt: Some(delivery_attempt), delivery_attempt: Some(delivery_attempt),
metadata: initial_event.metadata, metadata: initial_event.metadata,
is_overall_delivery_successful: Some(false),
}; };
let event = db let event = db

View File

@ -0,0 +1 @@
ALTER TABLE events DROP COLUMN IF EXISTS is_overall_delivery_successful;

View File

@ -0,0 +1 @@
ALTER TABLE events ADD COLUMN IF NOT EXISTS is_overall_delivery_successful BOOLEAN;