feat(webhooks): add support for custom outgoing webhook http headers (#5275)

Co-authored-by: Chikke Srujan <chikke.srujan@Chikke-Srujan-N7WRTY72X7.local>
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
chikke srujan
2024-07-16 21:43:44 +05:30
committed by GitHub
parent 29f8732d30
commit 101b21f52d
14 changed files with 365 additions and 224 deletions

View File

@ -612,7 +612,7 @@ async fn payments_incoming_webhook_flow(
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -623,7 +623,7 @@ async fn payments_incoming_webhook_flow(
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
primary_object_created_at,
)
))
.await?;
};
@ -735,7 +735,7 @@ async fn payouts_incoming_webhook_flow(
.attach_printable("Failed to fetch the payout create response")?,
};
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -746,7 +746,7 @@ async fn payouts_incoming_webhook_flow(
enums::EventObjectType::PayoutDetails,
api::OutgoingWebhookContent::PayoutDetails(payout_create_response),
Some(updated_payout_attempt.created_at),
)
))
.await?;
}
@ -840,7 +840,7 @@ async fn refunds_incoming_webhook_flow(
if let Some(outgoing_event_type) = event_type {
let refund_response: api_models::refunds::RefundResponse =
updated_refund.clone().foreign_into();
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -851,7 +851,7 @@ async fn refunds_incoming_webhook_flow(
enums::EventObjectType::RefundDetails,
api::OutgoingWebhookContent::RefundDetails(refund_response),
Some(updated_refund.created_at),
)
))
.await?;
}
@ -1116,7 +1116,7 @@ async fn external_authentication_incoming_webhook_flow(
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -1127,7 +1127,7 @@ async fn external_authentication_incoming_webhook_flow(
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
primary_object_created_at,
)
))
.await?;
};
let response = WebhookResponseTracker::Payment { payment_id, status };
@ -1214,7 +1214,7 @@ async fn mandates_incoming_webhook_flow(
);
let event_type: Option<enums::EventType> = updated_mandate.mandate_status.foreign_into();
if let Some(outgoing_event_type) = event_type {
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -1225,7 +1225,7 @@ async fn mandates_incoming_webhook_flow(
enums::EventObjectType::MandateDetails,
api::OutgoingWebhookContent::MandateDetails(mandates_response),
Some(updated_mandate.created_at),
)
))
.await?;
}
Ok(WebhookResponseTracker::Mandate {
@ -1323,7 +1323,7 @@ async fn frm_incoming_webhook_flow(
let event_type: Option<enums::EventType> = payments_response.status.foreign_into();
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -1334,7 +1334,7 @@ async fn frm_incoming_webhook_flow(
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
primary_object_created_at,
)
))
.await?;
};
let response = WebhookResponseTracker::Payment { payment_id, status };
@ -1397,7 +1397,7 @@ async fn disputes_incoming_webhook_flow(
let disputes_response = Box::new(dispute_object.clone().foreign_into());
let event_type: enums::EventType = dispute_object.dispute_status.foreign_into();
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -1408,7 +1408,7 @@ async fn disputes_incoming_webhook_flow(
enums::EventObjectType::DisputeDetails,
api::OutgoingWebhookContent::DisputeDetails(disputes_response),
Some(dispute_object.created_at),
)
))
.await?;
metrics::INCOMING_DISPUTE_WEBHOOK_MERCHANT_NOTIFIED_METRIC.add(&metrics::CONTEXT, 1, &[]);
Ok(WebhookResponseTracker::Dispute {
@ -1489,7 +1489,7 @@ async fn bank_transfer_webhook_flow(
// If event is NOT an UnsupportedEvent, trigger Outgoing Webhook
if let Some(outgoing_event_type) = event_type {
let primary_object_created_at = payments_response.created;
super::create_event_and_trigger_outgoing_webhook(
Box::pin(super::create_event_and_trigger_outgoing_webhook(
state,
merchant_account,
business_profile,
@ -1500,7 +1500,7 @@ async fn bank_transfer_webhook_flow(
enums::EventObjectType::PaymentDetails,
api::OutgoingWebhookContent::PaymentDetails(payments_response),
primary_object_created_at,
)
))
.await?;
}

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use api_models::{
webhook_events::{OutgoingWebhookRequestContent, OutgoingWebhookResponseContent},
webhooks,
@ -5,6 +7,7 @@ use api_models::{
use common_utils::{ext_traits::Encode, request::RequestContent};
use diesel_models::process_tracker::business_status;
use error_stack::{report, ResultExt};
use hyperswitch_domain_models::type_encryption::decrypt;
use masking::{ExposeInterface, Mask, PeekInterface, Secret};
use router_env::{
instrument,
@ -86,8 +89,10 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
let request_content = get_outgoing_webhook_request(
&merchant_account,
outgoing_webhook,
business_profile.payment_response_hash_key.as_deref(),
&business_profile,
merchant_key_store,
)
.await
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("Failed to construct outgoing webhook request content")?;
@ -546,15 +551,17 @@ fn get_webhook_url_from_business_profile(
.map(ExposeInterface::expose)
}
pub(crate) fn get_outgoing_webhook_request(
pub(crate) async fn get_outgoing_webhook_request(
merchant_account: &domain::MerchantAccount,
outgoing_webhook: api::OutgoingWebhook,
payment_response_hash_key: Option<&str>,
business_profile: &diesel_models::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
#[inline]
fn get_outgoing_webhook_request_inner<WebhookType: types::OutgoingWebhookType>(
async fn get_outgoing_webhook_request_inner<WebhookType: types::OutgoingWebhookType>(
outgoing_webhook: api::OutgoingWebhook,
payment_response_hash_key: Option<&str>,
business_profile: &diesel_models::business_profile::BusinessProfile,
key_store: &domain::MerchantKeyStore,
) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
let mut headers = vec![(
reqwest::header::CONTENT_TYPE.to_string(),
@ -562,7 +569,31 @@ pub(crate) fn get_outgoing_webhook_request(
)];
let transformed_outgoing_webhook = WebhookType::from(outgoing_webhook);
let payment_response_hash_key = business_profile.payment_response_hash_key.clone();
let custom_headers = decrypt::<serde_json::Value, masking::WithType>(
business_profile
.outgoing_webhook_custom_http_headers
.clone(),
key_store.key.get_inner().peek(),
)
.await
.change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed)
.attach_printable("Failed to decrypt outgoing webhook custom HTTP headers")?
.map(|decrypted_value| {
decrypted_value
.into_inner()
.expose()
.parse_value::<HashMap<String, String>>("HashMap<String,String>")
.change_context(errors::WebhooksFlowError::OutgoingWebhookEncodingFailed)
.attach_printable("Failed to deserialize outgoing webhook custom HTTP headers")
})
.transpose()?;
if let Some(ref map) = custom_headers {
headers.extend(
map.iter()
.map(|(key, value)| (key.clone(), value.clone().into_masked())),
);
};
let outgoing_webhooks_signature = transformed_outgoing_webhook
.get_outgoing_webhooks_signature(payment_response_hash_key)?;
@ -581,15 +612,22 @@ pub(crate) fn get_outgoing_webhook_request(
match merchant_account.get_compatible_connector() {
#[cfg(feature = "stripe")]
Some(api_models::enums::Connector::Stripe) => get_outgoing_webhook_request_inner::<
stripe_webhooks::StripeOutgoingWebhook,
>(
outgoing_webhook, payment_response_hash_key
),
_ => get_outgoing_webhook_request_inner::<webhooks::OutgoingWebhook>(
outgoing_webhook,
payment_response_hash_key,
),
Some(api_models::enums::Connector::Stripe) => {
get_outgoing_webhook_request_inner::<stripe_webhooks::StripeOutgoingWebhook>(
outgoing_webhook,
business_profile,
key_store,
)
.await
}
_ => {
get_outgoing_webhook_request_inner::<webhooks::OutgoingWebhook>(
outgoing_webhook,
business_profile,
key_store,
)
.await
}
}
}