mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 04:04:55 +08:00
feat(webhooks): allow manually retrying delivery of outgoing webhooks (#4176)
This commit is contained in:
@ -34,6 +34,7 @@ pub enum Permission {
|
||||
WebhookEventRead,
|
||||
PayoutWrite,
|
||||
PayoutRead,
|
||||
WebhookEventWrite,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
|
||||
@ -94,6 +94,14 @@ pub struct EventRetrieveResponse {
|
||||
pub delivery_attempt: Option<WebhookDeliveryAttempt>,
|
||||
}
|
||||
|
||||
impl common_utils::events::ApiEventMetric for EventRetrieveResponse {
|
||||
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
|
||||
Some(common_utils::events::ApiEventsType::Events {
|
||||
merchant_id_or_profile_id: self.event_information.merchant_id.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The request information (headers and body) sent in the webhook.
|
||||
#[derive(Debug, Serialize, Deserialize, ToSchema)]
|
||||
pub struct OutgoingWebhookRequestContent {
|
||||
@ -114,20 +122,24 @@ pub struct OutgoingWebhookRequestContent {
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize, ToSchema)]
|
||||
pub struct OutgoingWebhookResponseContent {
|
||||
/// The response body received for the webhook sent.
|
||||
#[schema(value_type = String)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
#[serde(alias = "payload")]
|
||||
pub body: Secret<String>,
|
||||
pub body: Option<Secret<String>>,
|
||||
|
||||
/// The response headers received for the webhook sent.
|
||||
#[schema(
|
||||
value_type = Vec<(String, String)>,
|
||||
value_type = Option<Vec<(String, String)>>,
|
||||
example = json!([["content-type", "application/json"], ["content-length", "1024"]]))
|
||||
]
|
||||
pub headers: Vec<(String, Secret<String>)>,
|
||||
pub headers: Option<Vec<(String, Secret<String>)>>,
|
||||
|
||||
/// The HTTP status code for the webhook sent.
|
||||
#[schema(example = 200)]
|
||||
pub status_code: u16,
|
||||
pub status_code: Option<u16>,
|
||||
|
||||
/// Error message in case any error occurred when trying to deliver the webhook.
|
||||
#[schema(example = 200)]
|
||||
pub error_message: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
@ -157,3 +169,17 @@ impl common_utils::events::ApiEventMetric for WebhookDeliveryAttemptListRequestI
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct WebhookDeliveryRetryRequestInternal {
|
||||
pub merchant_id_or_profile_id: String,
|
||||
pub event_id: String,
|
||||
}
|
||||
|
||||
impl common_utils::events::ApiEventMetric for WebhookDeliveryRetryRequestInternal {
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,6 +177,7 @@ Never share your secret api keys. Keep them guarded and secure.
|
||||
// Routes for events
|
||||
routes::webhook_events::list_initial_webhook_delivery_attempts,
|
||||
routes::webhook_events::list_webhook_delivery_attempts,
|
||||
routes::webhook_events::retry_webhook_delivery_attempt,
|
||||
),
|
||||
components(schemas(
|
||||
api_models::refunds::RefundRequest,
|
||||
|
||||
@ -68,3 +68,27 @@ pub fn list_initial_webhook_delivery_attempts() {}
|
||||
security(("admin_api_key" = []))
|
||||
)]
|
||||
pub fn list_webhook_delivery_attempts() {}
|
||||
|
||||
/// Events - Manual Retry
|
||||
///
|
||||
/// Manually retry the delivery of the specified Event.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/events/{merchant_id_or_profile_id}/{event_id}/retry",
|
||||
params(
|
||||
("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(
|
||||
(
|
||||
status = 200,
|
||||
description = "The delivery of the Event was attempted. \
|
||||
Check the `response` field in the response payload to identify the status of the delivery attempt.",
|
||||
body = EventRetrieveResponse
|
||||
),
|
||||
),
|
||||
tag = "Event",
|
||||
operation_id = "Manually retry the delivery of an Event",
|
||||
security(("admin_api_key" = []))
|
||||
)]
|
||||
pub fn retry_webhook_delivery_attempt() {}
|
||||
|
||||
@ -887,9 +887,69 @@ async fn trigger_webhook_to_merchant(
|
||||
);
|
||||
logger::debug!(outgoing_webhook_response=?response);
|
||||
|
||||
let update_event_if_client_error =
|
||||
|state: AppState,
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
merchant_id: String,
|
||||
event_id: String,
|
||||
error_message: String| async move {
|
||||
let is_webhook_notified = false;
|
||||
|
||||
let response_to_store = OutgoingWebhookResponseContent {
|
||||
body: None,
|
||||
headers: None,
|
||||
status_code: None,
|
||||
error_message: Some(error_message),
|
||||
};
|
||||
|
||||
let event_update = domain::EventUpdate::UpdateResponse {
|
||||
is_webhook_notified,
|
||||
response: Some(
|
||||
domain_types::encrypt(
|
||||
response_to_store
|
||||
.encode_to_string_of_json()
|
||||
.change_context(
|
||||
errors::WebhooksFlowError::OutgoingWebhookResponseEncodingFailed,
|
||||
)
|
||||
.map(Secret::new)?,
|
||||
merchant_key_store.key.get_inner().peek(),
|
||||
)
|
||||
.await
|
||||
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
|
||||
.attach_printable("Failed to encrypt outgoing webhook response content")?,
|
||||
),
|
||||
};
|
||||
|
||||
state
|
||||
.store
|
||||
.update_event_by_merchant_id_event_id(
|
||||
&merchant_id,
|
||||
&event_id,
|
||||
event_update,
|
||||
&merchant_key_store,
|
||||
)
|
||||
.await
|
||||
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
|
||||
};
|
||||
|
||||
let api_client_error_handler =
|
||||
|client_error: error_stack::Report<errors::ApiClientError>,
|
||||
delivery_attempt: enums::WebhookDeliveryAttempt| {
|
||||
|state: AppState,
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
merchant_id: String,
|
||||
event_id: String,
|
||||
client_error: error_stack::Report<errors::ApiClientError>,
|
||||
delivery_attempt: enums::WebhookDeliveryAttempt| async move {
|
||||
// Not including detailed error message in response information since it contains too
|
||||
// much of diagnostic information to be exposed to the merchant.
|
||||
update_event_if_client_error(
|
||||
state,
|
||||
merchant_key_store,
|
||||
merchant_id,
|
||||
event_id,
|
||||
"Unable to send request to merchant server".to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let error =
|
||||
client_error.change_context(errors::WebhooksFlowError::CallToMerchantFailed);
|
||||
logger::error!(
|
||||
@ -897,6 +957,8 @@ async fn trigger_webhook_to_merchant(
|
||||
?delivery_attempt,
|
||||
"An error occurred when sending webhook to merchant"
|
||||
);
|
||||
|
||||
Ok::<_, error_stack::Report<errors::WebhooksFlowError>>(())
|
||||
};
|
||||
let update_event_in_storage = |state: AppState,
|
||||
merchant_key_store: domain::MerchantKeyStore,
|
||||
@ -934,9 +996,10 @@ async fn trigger_webhook_to_merchant(
|
||||
Secret::from(String::from("Non-UTF-8 response body"))
|
||||
});
|
||||
let response_to_store = OutgoingWebhookResponseContent {
|
||||
body: response_body,
|
||||
headers: response_headers,
|
||||
status_code: status_code.as_u16(),
|
||||
body: Some(response_body),
|
||||
headers: Some(response_headers),
|
||||
status_code: Some(status_code.as_u16()),
|
||||
error_message: None,
|
||||
};
|
||||
|
||||
let event_update = domain::EventUpdate::UpdateResponse {
|
||||
@ -953,7 +1016,7 @@ async fn trigger_webhook_to_merchant(
|
||||
)
|
||||
.await
|
||||
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
|
||||
.attach_printable("Failed to encrypt outgoing webhook request content")?,
|
||||
.attach_printable("Failed to encrypt outgoing webhook response content")?,
|
||||
),
|
||||
};
|
||||
state
|
||||
@ -967,16 +1030,19 @@ async fn trigger_webhook_to_merchant(
|
||||
.await
|
||||
.change_context(errors::WebhooksFlowError::WebhookEventUpdationFailed)
|
||||
};
|
||||
let increment_webhook_outgoing_received_count = |merchant_id: String| {
|
||||
metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&[metrics::KeyValue::new(MERCHANT_ID, merchant_id)],
|
||||
)
|
||||
};
|
||||
let success_response_handler =
|
||||
|state: AppState,
|
||||
merchant_id: String,
|
||||
process_tracker: Option<storage::ProcessTracker>,
|
||||
business_status: &'static str| async move {
|
||||
metrics::WEBHOOK_OUTGOING_RECEIVED_COUNT.add(
|
||||
&metrics::CONTEXT,
|
||||
1,
|
||||
&[metrics::KeyValue::new(MERCHANT_ID, merchant_id)],
|
||||
);
|
||||
increment_webhook_outgoing_received_count(merchant_id);
|
||||
|
||||
match process_tracker {
|
||||
Some(process_tracker) => state
|
||||
@ -1006,7 +1072,17 @@ async fn trigger_webhook_to_merchant(
|
||||
|
||||
match delivery_attempt {
|
||||
enums::WebhookDeliveryAttempt::InitialAttempt => match response {
|
||||
Err(client_error) => api_client_error_handler(client_error, delivery_attempt),
|
||||
Err(client_error) => {
|
||||
api_client_error_handler(
|
||||
state.clone(),
|
||||
merchant_key_store.clone(),
|
||||
business_profile.merchant_id.clone(),
|
||||
event_id.clone(),
|
||||
client_error,
|
||||
delivery_attempt,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Ok(response) => {
|
||||
let status_code = response.status();
|
||||
let _updated_event = update_event_in_storage(
|
||||
@ -1043,7 +1119,15 @@ async fn trigger_webhook_to_merchant(
|
||||
.attach_printable("`process_tracker` is unavailable in automatic retry flow")?;
|
||||
match response {
|
||||
Err(client_error) => {
|
||||
api_client_error_handler(client_error, delivery_attempt);
|
||||
api_client_error_handler(
|
||||
state.clone(),
|
||||
merchant_key_store.clone(),
|
||||
business_profile.merchant_id.clone(),
|
||||
event_id.clone(),
|
||||
client_error,
|
||||
delivery_attempt,
|
||||
)
|
||||
.await?;
|
||||
// Schedule a retry attempt for webhook delivery
|
||||
outgoing_webhook_retry::retry_webhook_delivery_task(
|
||||
&*state.store,
|
||||
@ -1095,10 +1179,41 @@ async fn trigger_webhook_to_merchant(
|
||||
}
|
||||
}
|
||||
}
|
||||
enums::WebhookDeliveryAttempt::ManualRetry => {
|
||||
// Will be updated when manual retry is implemented
|
||||
Err(errors::WebhooksFlowError::NotReceivedByMerchant)?
|
||||
enums::WebhookDeliveryAttempt::ManualRetry => match response {
|
||||
Err(client_error) => {
|
||||
api_client_error_handler(
|
||||
state.clone(),
|
||||
merchant_key_store.clone(),
|
||||
business_profile.merchant_id.clone(),
|
||||
event_id.clone(),
|
||||
client_error,
|
||||
delivery_attempt,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
Ok(response) => {
|
||||
let status_code = response.status();
|
||||
let _updated_event = update_event_in_storage(
|
||||
state.clone(),
|
||||
merchant_key_store.clone(),
|
||||
business_profile.merchant_id.clone(),
|
||||
event_id.clone(),
|
||||
response,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if status_code.is_success() {
|
||||
increment_webhook_outgoing_received_count(business_profile.merchant_id.clone());
|
||||
} else {
|
||||
error_response_handler(
|
||||
business_profile.merchant_id,
|
||||
delivery_attempt,
|
||||
status_code.as_u16(),
|
||||
"Ignoring error when sending webhook to merchant",
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
use error_stack::ResultExt;
|
||||
use masking::PeekInterface;
|
||||
use router_env::{instrument, tracing};
|
||||
|
||||
use crate::{
|
||||
core::errors::{self, RouterResponse, StorageErrorExt},
|
||||
routes::AppState,
|
||||
services::ApplicationResponse,
|
||||
types::{api, domain, transformers::ForeignTryFrom},
|
||||
types::{api, domain, storage, transformers::ForeignTryFrom},
|
||||
utils::{OptionExt, StringExt},
|
||||
};
|
||||
|
||||
const INITIAL_DELIVERY_ATTEMPTS_LIST_MAX_LIMIT: i64 = 100;
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MerchantIdOrProfileId {
|
||||
MerchantId(String),
|
||||
ProfileId(String),
|
||||
enum MerchantAccountOrBusinessProfile {
|
||||
MerchantAccount(domain::MerchantAccount),
|
||||
BusinessProfile(storage::BusinessProfile),
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
@ -27,22 +29,22 @@ pub async fn list_initial_delivery_attempts(
|
||||
|
||||
let store = state.store.as_ref();
|
||||
|
||||
let (identifier, key_store) =
|
||||
let (account, 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 } => {
|
||||
match identifier {
|
||||
MerchantIdOrProfileId::MerchantId(merchant_id) => store
|
||||
match account {
|
||||
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => store
|
||||
.list_initial_events_by_merchant_id_primary_object_id(
|
||||
&merchant_id,
|
||||
&merchant_account.merchant_id,
|
||||
&object_id,
|
||||
&key_store,
|
||||
)
|
||||
.await,
|
||||
MerchantIdOrProfileId::ProfileId(profile_id) => store
|
||||
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => store
|
||||
.list_initial_events_by_profile_id_primary_object_id(
|
||||
&profile_id,
|
||||
&business_profile.profile_id,
|
||||
&object_id,
|
||||
&key_store,
|
||||
)
|
||||
@ -69,10 +71,10 @@ pub async fn list_initial_delivery_attempts(
|
||||
_ => None,
|
||||
};
|
||||
|
||||
match identifier {
|
||||
MerchantIdOrProfileId::MerchantId(merchant_id) => store
|
||||
match account {
|
||||
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => store
|
||||
.list_initial_events_by_merchant_id_constraints(
|
||||
&merchant_id,
|
||||
&merchant_account.merchant_id,
|
||||
created_after,
|
||||
created_before,
|
||||
limit,
|
||||
@ -80,9 +82,9 @@ pub async fn list_initial_delivery_attempts(
|
||||
&key_store,
|
||||
)
|
||||
.await,
|
||||
MerchantIdOrProfileId::ProfileId(profile_id) => store
|
||||
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => store
|
||||
.list_initial_events_by_profile_id_constraints(
|
||||
&profile_id,
|
||||
&business_profile.profile_id,
|
||||
created_after,
|
||||
created_before,
|
||||
limit,
|
||||
@ -112,23 +114,23 @@ pub async fn list_delivery_attempts(
|
||||
) -> RouterResponse<Vec<api::webhook_events::EventRetrieveResponse>> {
|
||||
let store = state.store.as_ref();
|
||||
|
||||
let (identifier, key_store) =
|
||||
let (account, key_store) =
|
||||
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;
|
||||
|
||||
let events = match identifier {
|
||||
MerchantIdOrProfileId::MerchantId(merchant_id) => {
|
||||
let events = match account {
|
||||
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account) => {
|
||||
store
|
||||
.list_events_by_merchant_id_initial_attempt_id(
|
||||
&merchant_id,
|
||||
&merchant_account.merchant_id,
|
||||
&initial_attempt_id,
|
||||
&key_store,
|
||||
)
|
||||
.await
|
||||
}
|
||||
MerchantIdOrProfileId::ProfileId(profile_id) => {
|
||||
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => {
|
||||
store
|
||||
.list_events_by_profile_id_initial_attempt_id(
|
||||
&profile_id,
|
||||
&business_profile.profile_id,
|
||||
&initial_attempt_id,
|
||||
&key_store,
|
||||
)
|
||||
@ -153,10 +155,108 @@ pub async fn list_delivery_attempts(
|
||||
}
|
||||
}
|
||||
|
||||
#[instrument(skip(state))]
|
||||
pub async fn retry_delivery_attempt(
|
||||
state: AppState,
|
||||
merchant_id_or_profile_id: String,
|
||||
event_id: String,
|
||||
) -> RouterResponse<api::webhook_events::EventRetrieveResponse> {
|
||||
let store = state.store.as_ref();
|
||||
|
||||
let (account, key_store) =
|
||||
determine_identifier_and_get_key_store(state.clone(), merchant_id_or_profile_id).await?;
|
||||
|
||||
let event_to_retry = store
|
||||
.find_event_by_merchant_id_event_id(&key_store.merchant_id, &event_id, &key_store)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::EventNotFound)?;
|
||||
|
||||
let business_profile = match account {
|
||||
MerchantAccountOrBusinessProfile::MerchantAccount(_) => {
|
||||
let business_profile_id = event_to_retry
|
||||
.business_profile_id
|
||||
.get_required_value("business_profile_id")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to read business profile ID from event to retry")?;
|
||||
store
|
||||
.find_business_profile_by_profile_id(&business_profile_id)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to find business profile")
|
||||
}
|
||||
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile) => Ok(business_profile),
|
||||
}?;
|
||||
|
||||
let delivery_attempt = storage::enums::WebhookDeliveryAttempt::ManualRetry;
|
||||
let new_event_id = super::utils::generate_event_id();
|
||||
let idempotent_event_id = super::utils::get_idempotent_event_id(
|
||||
&event_to_retry.primary_object_id,
|
||||
event_to_retry.event_type,
|
||||
delivery_attempt,
|
||||
);
|
||||
|
||||
let now = common_utils::date_time::now();
|
||||
let new_event = domain::Event {
|
||||
event_id: new_event_id.clone(),
|
||||
event_type: event_to_retry.event_type,
|
||||
event_class: event_to_retry.event_class,
|
||||
is_webhook_notified: false,
|
||||
primary_object_id: event_to_retry.primary_object_id,
|
||||
primary_object_type: event_to_retry.primary_object_type,
|
||||
created_at: now,
|
||||
merchant_id: Some(business_profile.merchant_id.clone()),
|
||||
business_profile_id: Some(business_profile.profile_id.clone()),
|
||||
primary_object_created_at: event_to_retry.primary_object_created_at,
|
||||
idempotent_event_id: Some(idempotent_event_id),
|
||||
initial_attempt_id: event_to_retry.initial_attempt_id,
|
||||
request: event_to_retry.request,
|
||||
response: None,
|
||||
delivery_attempt: Some(delivery_attempt),
|
||||
};
|
||||
|
||||
let event = store
|
||||
.insert_event(new_event, &key_store)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to insert event")?;
|
||||
|
||||
// We only allow retrying deliveries for events with `request` populated.
|
||||
let request_content = event
|
||||
.request
|
||||
.as_ref()
|
||||
.get_required_value("request")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?
|
||||
.peek()
|
||||
.parse_struct("OutgoingWebhookRequestContent")
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to parse webhook event request information")?;
|
||||
|
||||
super::trigger_webhook_and_raise_event(
|
||||
state.clone(),
|
||||
business_profile,
|
||||
&key_store,
|
||||
event,
|
||||
request_content,
|
||||
delivery_attempt,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
let updated_event = store
|
||||
.find_event_by_merchant_id_event_id(&key_store.merchant_id, &new_event_id, &key_store)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::EventNotFound)?;
|
||||
|
||||
Ok(ApplicationResponse::Json(
|
||||
api::webhook_events::EventRetrieveResponse::try_from(updated_event)?,
|
||||
))
|
||||
}
|
||||
|
||||
async fn determine_identifier_and_get_key_store(
|
||||
state: AppState,
|
||||
merchant_id_or_profile_id: String,
|
||||
) -> errors::RouterResult<(MerchantIdOrProfileId, domain::MerchantKeyStore)> {
|
||||
) -> errors::RouterResult<(MerchantAccountOrBusinessProfile, domain::MerchantKeyStore)> {
|
||||
let store = state.store.as_ref();
|
||||
match store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
@ -165,13 +265,25 @@ async fn determine_identifier_and_get_key_store(
|
||||
)
|
||||
.await
|
||||
{
|
||||
// Valid merchant ID
|
||||
Ok(key_store) => Ok((
|
||||
MerchantIdOrProfileId::MerchantId(merchant_id_or_profile_id),
|
||||
key_store,
|
||||
)),
|
||||
// Since a merchant key store was found with `merchant_id` = `merchant_id_or_profile_id`,
|
||||
// `merchant_id_or_profile_id` is a valid merchant ID.
|
||||
// Find a merchant account having `merchant_id` = `merchant_id_or_profile_id`.
|
||||
Ok(key_store) => {
|
||||
let merchant_account = store
|
||||
.find_merchant_account_by_merchant_id(&merchant_id_or_profile_id, &key_store)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||
|
||||
// Invalid merchant ID, check if we can find a business profile with the identifier
|
||||
Ok((
|
||||
MerchantAccountOrBusinessProfile::MerchantAccount(merchant_account),
|
||||
key_store,
|
||||
))
|
||||
}
|
||||
|
||||
// Since no merchant key store was found with `merchant_id` = `merchant_id_or_profile_id`,
|
||||
// `merchant_id_or_profile_id` is not a valid merchant ID.
|
||||
// Assuming that `merchant_id_or_profile_id` is a business profile ID, try to find a
|
||||
// business profile having `profile_id` = `merchant_id_or_profile_id`.
|
||||
Err(error) if error.current_context().is_db_not_found() => {
|
||||
router_env::logger::debug!(
|
||||
?error,
|
||||
@ -195,7 +307,7 @@ async fn determine_identifier_and_get_key_store(
|
||||
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
|
||||
|
||||
Ok((
|
||||
MerchantIdOrProfileId::ProfileId(business_profile.profile_id),
|
||||
MerchantAccountOrBusinessProfile::BusinessProfile(business_profile),
|
||||
key_store,
|
||||
))
|
||||
}
|
||||
|
||||
@ -1247,8 +1247,15 @@ impl WebhookEvents {
|
||||
.app_data(web::Data::new(config))
|
||||
.service(web::resource("").route(web::get().to(list_initial_webhook_delivery_attempts)))
|
||||
.service(
|
||||
web::resource("/{event_id}/attempts")
|
||||
web::scope("/{event_id}")
|
||||
.service(
|
||||
web::resource("attempts")
|
||||
.route(web::get().to(list_webhook_delivery_attempts)),
|
||||
)
|
||||
.service(
|
||||
web::resource("retry")
|
||||
.route(web::post().to(retry_webhook_delivery_attempt)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -137,7 +137,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
Flow::FrmFulfillment
|
||||
| Flow::IncomingWebhookReceive
|
||||
| Flow::WebhookEventInitialDeliveryAttemptList
|
||||
| Flow::WebhookEventDeliveryAttemptList => Self::Webhooks,
|
||||
| Flow::WebhookEventDeliveryAttemptList
|
||||
| Flow::WebhookEventDeliveryRetry => Self::Webhooks,
|
||||
|
||||
Flow::ApiKeyCreate
|
||||
| Flow::ApiKeyRetrieve
|
||||
|
||||
@ -7,6 +7,7 @@ use crate::{
|
||||
services::{api, authentication as auth, authorization::permissions::Permission},
|
||||
types::api::webhook_events::{
|
||||
EventListConstraints, EventListRequestInternal, WebhookDeliveryAttemptListRequestInternal,
|
||||
WebhookDeliveryRetryRequestInternal,
|
||||
},
|
||||
};
|
||||
|
||||
@ -89,3 +90,42 @@ pub async fn list_webhook_delivery_attempts(
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(skip_all, fields(flow = ?Flow::WebhookEventDeliveryRetry))]
|
||||
pub async fn retry_webhook_delivery_attempt(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
path: web::Path<(String, String)>,
|
||||
) -> impl Responder {
|
||||
let flow = Flow::WebhookEventDeliveryRetry;
|
||||
let (merchant_id_or_profile_id, event_id) = path.into_inner();
|
||||
|
||||
let request_internal = WebhookDeliveryRetryRequestInternal {
|
||||
merchant_id_or_profile_id: merchant_id_or_profile_id.clone(),
|
||||
event_id,
|
||||
};
|
||||
|
||||
api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
request_internal,
|
||||
|state, _, request_internal| {
|
||||
webhook_events::retry_delivery_attempt(
|
||||
state,
|
||||
request_internal.merchant_id_or_profile_id,
|
||||
request_internal.event_id,
|
||||
)
|
||||
},
|
||||
auth::auth_type(
|
||||
&auth::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantOrProfileFromRoute {
|
||||
merchant_id_or_profile_id,
|
||||
required_permission: Permission::WebhookEventWrite,
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
@ -75,12 +75,13 @@ pub static USERS_MANAGE: [Permission; 2] =
|
||||
|
||||
pub static MERCHANT_DETAILS_VIEW: [Permission; 1] = [Permission::MerchantAccountRead];
|
||||
|
||||
pub static MERCHANT_DETAILS_MANAGE: [Permission; 5] = [
|
||||
pub static MERCHANT_DETAILS_MANAGE: [Permission; 6] = [
|
||||
Permission::MerchantAccountWrite,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::ApiKeyWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
Permission::WebhookEventRead,
|
||||
Permission::WebhookEventWrite,
|
||||
];
|
||||
|
||||
pub static ORGANIZATION_MANAGE: [Permission; 2] = [
|
||||
|
||||
@ -31,6 +31,7 @@ pub enum Permission {
|
||||
UsersWrite,
|
||||
MerchantAccountCreate,
|
||||
WebhookEventRead,
|
||||
WebhookEventWrite,
|
||||
PayoutRead,
|
||||
PayoutWrite,
|
||||
}
|
||||
@ -71,6 +72,7 @@ impl Permission {
|
||||
Self::UsersWrite => "Invite users, assign and update roles",
|
||||
Self::MerchantAccountCreate => "Create merchant account",
|
||||
Self::WebhookEventRead => "View webhook events",
|
||||
Self::WebhookEventWrite => "Trigger retries for webhook events",
|
||||
Self::PayoutRead => "View all payouts",
|
||||
Self::PayoutWrite => "Create payout, download payout data",
|
||||
}
|
||||
|
||||
@ -2,4 +2,5 @@ pub use api_models::webhook_events::{
|
||||
EventListConstraints, EventListConstraintsInternal, EventListItemResponse,
|
||||
EventListRequestInternal, EventRetrieveResponse, OutgoingWebhookRequestContent,
|
||||
OutgoingWebhookResponseContent, WebhookDeliveryAttemptListRequestInternal,
|
||||
WebhookDeliveryRetryRequestInternal,
|
||||
};
|
||||
|
||||
@ -44,6 +44,7 @@ impl From<Permission> for user_role_api::Permission {
|
||||
Permission::UsersWrite => Self::UsersWrite,
|
||||
Permission::MerchantAccountCreate => Self::MerchantAccountCreate,
|
||||
Permission::WebhookEventRead => Self::WebhookEventRead,
|
||||
Permission::WebhookEventWrite => Self::WebhookEventWrite,
|
||||
Permission::PayoutRead => Self::PayoutRead,
|
||||
Permission::PayoutWrite => Self::PayoutWrite,
|
||||
}
|
||||
|
||||
@ -120,7 +120,7 @@ impl ProcessTrackerWorkflow<AppState> for OutgoingWebhookRetryWorkflow {
|
||||
&key_store,
|
||||
event,
|
||||
request_content,
|
||||
storage::enums::WebhookDeliveryAttempt::AutomaticRetry,
|
||||
delivery_attempt,
|
||||
None,
|
||||
Some(process),
|
||||
)
|
||||
@ -172,7 +172,7 @@ impl ProcessTrackerWorkflow<AppState> for OutgoingWebhookRetryWorkflow {
|
||||
&key_store,
|
||||
event,
|
||||
request_content,
|
||||
storage::enums::WebhookDeliveryAttempt::AutomaticRetry,
|
||||
delivery_attempt,
|
||||
Some(content),
|
||||
Some(process),
|
||||
)
|
||||
|
||||
@ -398,6 +398,8 @@ pub enum Flow {
|
||||
WebhookEventInitialDeliveryAttemptList,
|
||||
/// List delivery attempts for a webhook event
|
||||
WebhookEventDeliveryAttemptList,
|
||||
/// Manually retry the delivery for a webhook event
|
||||
WebhookEventDeliveryRetry,
|
||||
}
|
||||
|
||||
///
|
||||
|
||||
@ -4469,6 +4469,53 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/events/{merchant_id_or_profile_id}/{event_id}/retry": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"Event"
|
||||
],
|
||||
"summary": "Events - Manual Retry",
|
||||
"description": "Events - Manual Retry\n\nManually retry the delivery of the specified Event.",
|
||||
"operationId": "Manually retry the delivery of an Event",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "merchant_id_or_profile_id",
|
||||
"in": "path",
|
||||
"description": "The unique identifier for the Merchant Account or Business Profile",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "event_id",
|
||||
"in": "path",
|
||||
"description": "The unique identifier for the Event",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The delivery of the Event was attempted. Check the `response` field in the response payload to identify the status of the delivery attempt.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/EventRetrieveResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"admin_api_key": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"components": {
|
||||
@ -11661,15 +11708,11 @@
|
||||
"OutgoingWebhookResponseContent": {
|
||||
"type": "object",
|
||||
"description": "The response information (headers, body and status code) received for the webhook sent.",
|
||||
"required": [
|
||||
"body",
|
||||
"headers",
|
||||
"status_code"
|
||||
],
|
||||
"properties": {
|
||||
"body": {
|
||||
"type": "string",
|
||||
"description": "The response body received for the webhook sent."
|
||||
"description": "The response body received for the webhook sent.",
|
||||
"nullable": true
|
||||
},
|
||||
"headers": {
|
||||
"type": "array",
|
||||
@ -11696,14 +11739,22 @@
|
||||
"content-length",
|
||||
"1024"
|
||||
]
|
||||
]
|
||||
],
|
||||
"nullable": true
|
||||
},
|
||||
"status_code": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "The HTTP status code for the webhook sent.",
|
||||
"example": 200,
|
||||
"nullable": true,
|
||||
"minimum": 0
|
||||
},
|
||||
"error_message": {
|
||||
"type": "string",
|
||||
"description": "Error message in case any error occurred when trying to deliver the webhook.",
|
||||
"example": 200,
|
||||
"nullable": true
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user