Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(events): allow listing webhook events and webhook delivery attempts by business profile #4159

Merged
merged 3 commits into from
Mar 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
SanchithHegde marked this conversation as resolved.
Show resolved Hide resolved
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
Loading