Skip to content

Commit

Permalink
feat(events): allow listing webhook events and webhook delivery attem…
Browse files Browse the repository at this point in the history
…pts by business profile (juspay#4159)
  • Loading branch information
SanchithHegde committed Mar 22, 2024
1 parent 13fe584 commit 4c8cdf1
Show file tree
Hide file tree
Showing 9 changed files with 233 additions and 58 deletions.
18 changes: 16 additions & 2 deletions crates/api_models/src/webhook_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,28 @@ pub struct OutgoingWebhookResponseContent {

#[derive(Debug, serde::Serialize)]
pub struct EventListRequestInternal {
pub merchant_id: String,
pub merchant_id_or_profile_id: String,
pub constraints: EventListConstraints,
}

impl common_utils::events::ApiEventMetric for EventListRequestInternal {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Events {
merchant_id: self.merchant_id.clone(),
merchant_id_or_profile_id: self.merchant_id_or_profile_id.clone(),
})
}
}

#[derive(Debug, serde::Serialize)]
pub struct WebhookDeliveryAttemptListRequestInternal {
pub merchant_id_or_profile_id: String,
pub initial_attempt_id: String,
}

impl common_utils::events::ApiEventMetric for WebhookDeliveryAttemptListRequestInternal {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::Events {
merchant_id_or_profile_id: self.merchant_id_or_profile_id.clone(),
})
}
}
2 changes: 1 addition & 1 deletion crates/common_utils/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub enum ApiEventsType {
dispute_id: String,
},
Events {
merchant_id: String,
merchant_id_or_profile_id: String,
},
}

Expand Down
14 changes: 7 additions & 7 deletions crates/openapi/src/routes/webhook_events.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/// Events - List
///
/// List all Events associated with a Merchant Account.
/// List all Events associated with a Merchant Account or Business Profile.
#[utoipa::path(
get,
path = "/events/{merchant_id}",
path = "/events/{merchant_id_or_profile_id}",
params(
(
"merchant_id" = String,
"merchant_id_or_profile_id" = String,
Path,
description = "The unique identifier for the Merchant Account"
description = "The unique identifier for the Merchant Account or Business Profile"
),
(
"created_after" = Option<PrimitiveDateTime>,
Expand Down Expand Up @@ -45,7 +45,7 @@
(status = 200, description = "List of Events retrieved successfully", body = Vec<EventListItemResponse>),
),
tag = "Event",
operation_id = "List all Events associated with a Merchant Account",
operation_id = "List all Events associated with a Merchant Account or Business Profile",
security(("admin_api_key" = []))
)]
pub fn list_initial_webhook_delivery_attempts() {}
Expand All @@ -55,9 +55,9 @@ pub fn list_initial_webhook_delivery_attempts() {}
/// List all delivery attempts for the specified Event.
#[utoipa::path(
get,
path = "/events/{merchant_id}/{event_id}/attempts",
path = "/events/{merchant_id_or_profile_id}/{event_id}/attempts",
params(
("merchant_id" = String, Path, description = "The unique identifier for the Merchant Account"),
("merchant_id_or_profile_id" = String, Path, description = "The unique identifier for the Merchant Account or Business Profile"),
("event_id" = String, Path, description = "The unique identifier for the Event"),
),
responses(
Expand Down
140 changes: 114 additions & 26 deletions crates/router/src/core/webhooks/webhook_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,40 +5,49 @@ use crate::{
core::errors::{self, RouterResponse, StorageErrorExt},
routes::AppState,
services::ApplicationResponse,
types::{api, transformers::ForeignTryFrom},
types::{api, domain, transformers::ForeignTryFrom},
};

const INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT: i64 = 100;

#[derive(Debug)]
enum MerchantIdOrProfileId {
MerchantId(String),
ProfileId(String),
}

#[instrument(skip(state))]
pub async fn list_initial_delivery_attempts(
state: AppState,
merchant_id: String,
merchant_id_or_profile_id: String,
constraints: api::webhook_events::EventListConstraints,
) -> RouterResponse<Vec<api::webhook_events::EventListItemResponse>> {
let constraints =
api::webhook_events::EventListConstraintsInternal::foreign_try_from(constraints)?;

let store = state.store.as_ref();

// This would handle verifying that the merchant ID actually exists
let key_store = store
.get_merchant_key_store_by_merchant_id(
&merchant_id,
&store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let (identifier, key_store) =
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;

let events = match constraints {
api_models::webhook_events::EventListConstraintsInternal::ObjectIdFilter { object_id } => {
store
match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => store
.list_initial_events_by_merchant_id_primary_object_id(
&merchant_id,
&object_id,
&key_store,
)
.await
.await,
MerchantIdOrProfileId::ProfileId(profile_id) => store
.list_initial_events_by_profile_id_primary_object_id(
&profile_id,
&object_id,
&key_store,
)
.await,
}
}
api_models::webhook_events::EventListConstraintsInternal::GenericFilter {
created_after,
Expand All @@ -60,7 +69,8 @@ pub async fn list_initial_delivery_attempts(
_ => None,
};

store
match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => store
.list_initial_events_by_merchant_id_constraints(
&merchant_id,
created_after,
Expand All @@ -69,7 +79,18 @@ pub async fn list_initial_delivery_attempts(
offset,
&key_store,
)
.await
.await,
MerchantIdOrProfileId::ProfileId(profile_id) => store
.list_initial_events_by_profile_id_constraints(
&profile_id,
created_after,
created_before,
limit,
offset,
&key_store,
)
.await,
}
}
}
.change_context(errors::ApiErrorResponse::InternalServerError)
Expand All @@ -86,22 +107,36 @@ pub async fn list_initial_delivery_attempts(
#[instrument(skip(state))]
pub async fn list_delivery_attempts(
state: AppState,
merchant_id: &str,
initial_attempt_id: &str,
merchant_id_or_profile_id: String,
initial_attempt_id: String,
) -> RouterResponse<Vec<api::webhook_events::EventRetrieveResponse>> {
let store = state.store.as_ref();

// This would handle verifying that the merchant ID actually exists
let key_store = store
.get_merchant_key_store_by_merchant_id(merchant_id, &store.get_master_key().to_vec().into())
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let (identifier, key_store) =
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;

let events = store
.list_events_by_merchant_id_initial_attempt_id(merchant_id, initial_attempt_id, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to list delivery attempts for initial event")?;
let events = match identifier {
MerchantIdOrProfileId::MerchantId(merchant_id) => {
store
.list_events_by_merchant_id_initial_attempt_id(
&merchant_id,
&initial_attempt_id,
&key_store,
)
.await
}
MerchantIdOrProfileId::ProfileId(profile_id) => {
store
.list_events_by_profile_id_initial_attempt_id(
&profile_id,
&initial_attempt_id,
&key_store,
)
.await
}
}
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to list delivery attempts for initial event")?;

if events.is_empty() {
Err(error_stack::report!(
Expand All @@ -117,3 +152,56 @@ pub async fn list_delivery_attempts(
))
}
}

async fn determine_identifier_and_get_key_store(
state: AppState,
merchant_id_or_profile_id: String,
) -> errors::RouterResult<(MerchantIdOrProfileId, domain::MerchantKeyStore)> {
let store = state.store.as_ref();
match store
.get_merchant_key_store_by_merchant_id(
&merchant_id_or_profile_id,
&store.get_master_key().to_vec().into(),
)
.await
{
// Valid merchant ID
Ok(key_store) => Ok((
MerchantIdOrProfileId::MerchantId(merchant_id_or_profile_id),
key_store,
)),

// Invalid merchant ID, check if we can find a business profile with the identifier
Err(error) if error.current_context().is_db_not_found() => {
router_env::logger::debug!(
?error,
%merchant_id_or_profile_id,
"Failed to find merchant key store for the specified merchant ID or business profile ID"
);

let business_profile = store
.find_business_profile_by_profile_id(&merchant_id_or_profile_id)
.await
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
id: merchant_id_or_profile_id,
})?;

let key_store = store
.get_merchant_key_store_by_merchant_id(
&business_profile.merchant_id,
&store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;

Ok((
MerchantIdOrProfileId::ProfileId(business_profile.profile_id),
key_store,
))
}

Err(error) => Err(error)
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to find merchant key store by merchant ID"),
}
}
2 changes: 1 addition & 1 deletion crates/router/src/routes/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,7 +1224,7 @@ pub struct WebhookEvents;
#[cfg(feature = "olap")]
impl WebhookEvents {
pub fn server(config: AppState) -> Scope {
web::scope("/events/{merchant_id}")
web::scope("/events/{merchant_id_or_profile_id}")
.app_data(web::Data::new(config))
.service(web::resource("").route(web::get().to(list_initial_webhook_delivery_attempts)))
.service(
Expand Down
35 changes: 23 additions & 12 deletions crates/router/src/routes/webhook_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use crate::{
core::{api_locking, webhooks::webhook_events},
routes::AppState,
services::{api, authentication as auth, authorization::permissions::Permission},
types::api::webhook_events::{EventListConstraints, EventListRequestInternal},
types::api::webhook_events::{
EventListConstraints, EventListRequestInternal, WebhookDeliveryAttemptListRequestInternal,
},
};

#[instrument(skip_all, fields(flow = ?Flow::WebhookEventInitialDeliveryAttemptList))]
Expand All @@ -16,11 +18,11 @@ pub async fn list_initial_webhook_delivery_attempts(
query: web::Query<EventListConstraints>,
) -> impl Responder {
let flow = Flow::WebhookEventInitialDeliveryAttemptList;
let merchant_id = path.into_inner();
let merchant_id_or_profile_id = path.into_inner();
let constraints = query.into_inner();

let request_internal = EventListRequestInternal {
merchant_id: merchant_id.clone(),
merchant_id_or_profile_id: merchant_id_or_profile_id.clone(),
constraints,
};

Expand All @@ -32,14 +34,14 @@ pub async fn list_initial_webhook_delivery_attempts(
|state, _, request_internal| {
webhook_events::list_initial_delivery_attempts(
state,
request_internal.merchant_id,
request_internal.merchant_id_or_profile_id,
request_internal.constraints,
)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id,
&auth::JWTAuthMerchantOrProfileFromRoute {
merchant_id_or_profile_id,
required_permission: Permission::WebhookEventRead,
},
req.headers(),
Expand All @@ -56,20 +58,29 @@ pub async fn list_webhook_delivery_attempts(
path: web::Path<(String, String)>,
) -> impl Responder {
let flow = Flow::WebhookEventDeliveryAttemptList;
let (merchant_id, initial_event_id) = path.into_inner();
let (merchant_id_or_profile_id, initial_attempt_id) = path.into_inner();

let request_internal = WebhookDeliveryAttemptListRequestInternal {
merchant_id_or_profile_id: merchant_id_or_profile_id.clone(),
initial_attempt_id,
};

api::server_wrap(
flow,
state,
&req,
(&merchant_id, &initial_event_id),
|state, _, (merchant_id, initial_event_id)| {
webhook_events::list_delivery_attempts(state, merchant_id, initial_event_id)
request_internal,
|state, _, request_internal| {
webhook_events::list_delivery_attempts(
state,
request_internal.merchant_id_or_profile_id,
request_internal.initial_attempt_id,
)
},
auth::auth_type(
&auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute {
merchant_id: merchant_id.clone(),
&auth::JWTAuthMerchantOrProfileFromRoute {
merchant_id_or_profile_id,
required_permission: Permission::WebhookEventRead,
},
req.headers(),
Expand Down
Loading

0 comments on commit 4c8cdf1

Please sign in to comment.