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

@ -7006,6 +7006,11 @@
} }
], ],
"nullable": true "nullable": true
},
"outgoing_webhook_custom_http_headers": {
"type": "object",
"description": "These key-value pairs are sent as additional custom headers in the outgoing webhook request. It is recommended not to use more than four key-value pairs.",
"nullable": true
} }
}, },
"additionalProperties": false "additionalProperties": false
@ -7164,6 +7169,11 @@
} }
], ],
"nullable": true "nullable": true
},
"outgoing_webhook_custom_http_headers": {
"type": "object",
"description": "These key-value pairs are sent as additional custom headers in the outgoing webhook request.",
"nullable": true
} }
} }
}, },

View File

@ -1180,6 +1180,10 @@ pub struct BusinessProfileCreate {
/// Default payout link config /// Default payout link config
#[schema(value_type = Option<BusinessPayoutLinkConfig>)] #[schema(value_type = Option<BusinessPayoutLinkConfig>)]
pub payout_link_config: Option<BusinessPayoutLinkConfig>, pub payout_link_config: Option<BusinessPayoutLinkConfig>,
/// These key-value pairs are sent as additional custom headers in the outgoing webhook request. It is recommended not to use more than four key-value pairs.
#[schema(value_type = Option<Object>, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)]
pub outgoing_webhook_custom_http_headers: Option<HashMap<String, String>>,
} }
#[derive(Clone, Debug, ToSchema, Serialize)] #[derive(Clone, Debug, ToSchema, Serialize)]
@ -1272,6 +1276,10 @@ pub struct BusinessProfileResponse {
/// Default payout link config /// Default payout link config
#[schema(value_type = Option<BusinessPayoutLinkConfig>)] #[schema(value_type = Option<BusinessPayoutLinkConfig>)]
pub payout_link_config: Option<BusinessPayoutLinkConfig>, pub payout_link_config: Option<BusinessPayoutLinkConfig>,
/// These key-value pairs are sent as additional custom headers in the outgoing webhook request.
#[schema(value_type = Option<Object>, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)]
pub outgoing_webhook_custom_http_headers: Option<HashMap<String, String>>,
} }
#[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)]
@ -1356,6 +1364,10 @@ pub struct BusinessProfileUpdate {
/// Default payout link config /// Default payout link config
#[schema(value_type = Option<BusinessPayoutLinkConfig>)] #[schema(value_type = Option<BusinessPayoutLinkConfig>)]
pub payout_link_config: Option<BusinessPayoutLinkConfig>, pub payout_link_config: Option<BusinessPayoutLinkConfig>,
/// These key-value pairs are sent as additional custom headers in the outgoing webhook request. It is recommended not to use more than four key-value pairs.
#[schema(value_type = Option<Object>, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)]
pub outgoing_webhook_custom_http_headers: Option<HashMap<String, String>>,
} }
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)]
pub struct BusinessCollectLinkConfig { pub struct BusinessCollectLinkConfig {

View File

@ -1,7 +1,7 @@
use common_utils::pii; use common_utils::pii;
use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
use crate::schema::business_profile; use crate::{encryption::Encryption, schema::business_profile};
#[derive( #[derive(
Clone, Clone,
@ -43,6 +43,7 @@ pub struct BusinessProfile {
pub use_billing_as_payment_method_billing: Option<bool>, pub use_billing_as_payment_method_billing: Option<bool>,
pub collect_shipping_details_from_wallet_connector: Option<bool>, pub collect_shipping_details_from_wallet_connector: Option<bool>,
pub collect_billing_details_from_wallet_connector: Option<bool>, pub collect_billing_details_from_wallet_connector: Option<bool>,
pub outgoing_webhook_custom_http_headers: Option<Encryption>,
} }
#[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)]
@ -76,6 +77,7 @@ pub struct BusinessProfileNew {
pub use_billing_as_payment_method_billing: Option<bool>, pub use_billing_as_payment_method_billing: Option<bool>,
pub collect_shipping_details_from_wallet_connector: Option<bool>, pub collect_shipping_details_from_wallet_connector: Option<bool>,
pub collect_billing_details_from_wallet_connector: Option<bool>, pub collect_billing_details_from_wallet_connector: Option<bool>,
pub outgoing_webhook_custom_http_headers: Option<Encryption>,
} }
#[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)]
@ -106,6 +108,7 @@ pub struct BusinessProfileUpdateInternal {
pub use_billing_as_payment_method_billing: Option<bool>, pub use_billing_as_payment_method_billing: Option<bool>,
pub collect_shipping_details_from_wallet_connector: Option<bool>, pub collect_shipping_details_from_wallet_connector: Option<bool>,
pub collect_billing_details_from_wallet_connector: Option<bool>, pub collect_billing_details_from_wallet_connector: Option<bool>,
pub outgoing_webhook_custom_http_headers: Option<Encryption>,
} }
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@ -134,6 +137,7 @@ pub enum BusinessProfileUpdate {
collect_shipping_details_from_wallet_connector: Option<bool>, collect_shipping_details_from_wallet_connector: Option<bool>,
collect_billing_details_from_wallet_connector: Option<bool>, collect_billing_details_from_wallet_connector: Option<bool>,
is_connector_agnostic_mit_enabled: Option<bool>, is_connector_agnostic_mit_enabled: Option<bool>,
outgoing_webhook_custom_http_headers: Option<Encryption>,
}, },
ExtendedCardInfoUpdate { ExtendedCardInfoUpdate {
is_extended_card_info_enabled: Option<bool>, is_extended_card_info_enabled: Option<bool>,
@ -170,6 +174,7 @@ impl From<BusinessProfileUpdate> for BusinessProfileUpdateInternal {
collect_shipping_details_from_wallet_connector, collect_shipping_details_from_wallet_connector,
collect_billing_details_from_wallet_connector, collect_billing_details_from_wallet_connector,
is_connector_agnostic_mit_enabled, is_connector_agnostic_mit_enabled,
outgoing_webhook_custom_http_headers,
} => Self { } => Self {
profile_name, profile_name,
modified_at, modified_at,
@ -194,6 +199,7 @@ impl From<BusinessProfileUpdate> for BusinessProfileUpdateInternal {
collect_shipping_details_from_wallet_connector, collect_shipping_details_from_wallet_connector,
collect_billing_details_from_wallet_connector, collect_billing_details_from_wallet_connector,
is_connector_agnostic_mit_enabled, is_connector_agnostic_mit_enabled,
outgoing_webhook_custom_http_headers,
..Default::default() ..Default::default()
}, },
BusinessProfileUpdate::ExtendedCardInfoUpdate { BusinessProfileUpdate::ExtendedCardInfoUpdate {
@ -244,6 +250,7 @@ impl From<BusinessProfileNew> for BusinessProfile {
.collect_shipping_details_from_wallet_connector, .collect_shipping_details_from_wallet_connector,
collect_billing_details_from_wallet_connector: new collect_billing_details_from_wallet_connector: new
.collect_billing_details_from_wallet_connector, .collect_billing_details_from_wallet_connector,
outgoing_webhook_custom_http_headers: new.outgoing_webhook_custom_http_headers,
} }
} }
} }
@ -275,6 +282,7 @@ impl BusinessProfileUpdate {
use_billing_as_payment_method_billing, use_billing_as_payment_method_billing,
collect_shipping_details_from_wallet_connector, collect_shipping_details_from_wallet_connector,
collect_billing_details_from_wallet_connector, collect_billing_details_from_wallet_connector,
outgoing_webhook_custom_http_headers,
} = self.into(); } = self.into();
BusinessProfile { BusinessProfile {
profile_name: profile_name.unwrap_or(source.profile_name), profile_name: profile_name.unwrap_or(source.profile_name),
@ -303,6 +311,7 @@ impl BusinessProfileUpdate {
use_billing_as_payment_method_billing, use_billing_as_payment_method_billing,
collect_shipping_details_from_wallet_connector, collect_shipping_details_from_wallet_connector,
collect_billing_details_from_wallet_connector, collect_billing_details_from_wallet_connector,
outgoing_webhook_custom_http_headers,
..source ..source
} }
} }

View File

@ -204,6 +204,7 @@ diesel::table! {
use_billing_as_payment_method_billing -> Nullable<Bool>, use_billing_as_payment_method_billing -> Nullable<Bool>,
collect_shipping_details_from_wallet_connector -> Nullable<Bool>, collect_shipping_details_from_wallet_connector -> Nullable<Bool>,
collect_billing_details_from_wallet_connector -> Nullable<Bool>, collect_billing_details_from_wallet_connector -> Nullable<Bool>,
outgoing_webhook_custom_http_headers -> Nullable<Bytea>,
} }
} }

View File

@ -204,6 +204,7 @@ diesel::table! {
use_billing_as_payment_method_billing -> Nullable<Bool>, use_billing_as_payment_method_billing -> Nullable<Bool>,
collect_shipping_details_from_wallet_connector -> Nullable<Bool>, collect_shipping_details_from_wallet_connector -> Nullable<Bool>,
collect_billing_details_from_wallet_connector -> Nullable<Bool>, collect_billing_details_from_wallet_connector -> Nullable<Bool>,
outgoing_webhook_custom_http_headers -> Nullable<Bytea>,
} }
} }

View File

@ -25,7 +25,7 @@ use crate::{
core::{ core::{
encryption::transfer_encryption_key, encryption::transfer_encryption_key,
errors::{self, RouterResponse, RouterResult, StorageErrorExt}, errors::{self, RouterResponse, RouterResult, StorageErrorExt},
payment_methods::{cards, transformers}, payment_methods::{cards, cards::create_encrypted_data, transformers},
payments::helpers, payments::helpers,
pm_auth::helpers::PaymentAuthConnectorDataExt, pm_auth::helpers::PaymentAuthConnectorDataExt,
routing::helpers as routing_helpers, routing::helpers as routing_helpers,
@ -35,7 +35,8 @@ use crate::{
routes::{metrics, SessionState}, routes::{metrics, SessionState},
services::{self, api as service_api, authentication, pm_auth as payment_initiation_service}, services::{self, api as service_api, authentication, pm_auth as payment_initiation_service},
types::{ types::{
self, api, self,
api::{self, admin},
domain::{ domain::{
self, self,
types::{self as domain_types, AsyncLift}, types::{self as domain_types, AsyncLift},
@ -239,7 +240,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
.create_or_validate(db) .create_or_validate(db)
.await?; .await?;
let key = key_store.key.into_inner(); let key = key_store.key.clone().into_inner();
let mut merchant_account = async { let mut merchant_account = async {
Ok::<_, error_stack::Report<common_utils::errors::CryptoError>>( Ok::<_, error_stack::Report<common_utils::errors::CryptoError>>(
@ -292,7 +293,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
.change_context(errors::ApiErrorResponse::InternalServerError)?; .change_context(errors::ApiErrorResponse::InternalServerError)?;
CreateBusinessProfile::new(self.primary_business_details.clone()) CreateBusinessProfile::new(self.primary_business_details.clone())
.create_business_profiles(db, &mut merchant_account) .create_business_profiles(db, &mut merchant_account, &key_store)
.await?; .await?;
Ok(merchant_account) Ok(merchant_account)
@ -386,6 +387,7 @@ impl CreateBusinessProfile {
&self, &self,
db: &dyn StorageInterface, db: &dyn StorageInterface,
merchant_account: &mut domain::MerchantAccount, merchant_account: &mut domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<()> { ) -> RouterResult<()> {
match self { match self {
Self::CreateFromPrimaryBusinessDetails { Self::CreateFromPrimaryBusinessDetails {
@ -395,6 +397,7 @@ impl CreateBusinessProfile {
db, db,
merchant_account.clone(), merchant_account.clone(),
primary_business_details, primary_business_details,
key_store,
) )
.await?; .await?;
@ -407,7 +410,7 @@ impl CreateBusinessProfile {
} }
Self::CreateDefaultBusinessProfile => { Self::CreateDefaultBusinessProfile => {
let business_profile = self let business_profile = self
.create_default_business_profile(db, merchant_account.clone()) .create_default_business_profile(db, merchant_account.clone(), key_store)
.await?; .await?;
merchant_account.default_profile = Some(business_profile.profile_id); merchant_account.default_profile = Some(business_profile.profile_id);
@ -422,11 +425,13 @@ impl CreateBusinessProfile {
&self, &self,
db: &dyn StorageInterface, db: &dyn StorageInterface,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<diesel_models::business_profile::BusinessProfile> { ) -> RouterResult<diesel_models::business_profile::BusinessProfile> {
let business_profile = create_and_insert_business_profile( let business_profile = create_and_insert_business_profile(
db, db,
api_models::admin::BusinessProfileCreate::default(), api_models::admin::BusinessProfileCreate::default(),
merchant_account.clone(), merchant_account.clone(),
key_store,
) )
.await?; .await?;
@ -440,6 +445,7 @@ impl CreateBusinessProfile {
db: &dyn StorageInterface, db: &dyn StorageInterface,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
primary_business_details: &Vec<admin_types::PrimaryBusinessDetails>, primary_business_details: &Vec<admin_types::PrimaryBusinessDetails>,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<Vec<diesel_models::business_profile::BusinessProfile>> { ) -> RouterResult<Vec<diesel_models::business_profile::BusinessProfile>> {
let mut business_profiles_vector = Vec::with_capacity(primary_business_details.len()); let mut business_profiles_vector = Vec::with_capacity(primary_business_details.len());
@ -459,6 +465,7 @@ impl CreateBusinessProfile {
db, db,
business_profile_create_request, business_profile_create_request,
merchant_account.clone(), merchant_account.clone(),
key_store,
) )
.await .await
.map_err(|business_profile_insert_error| { .map_err(|business_profile_insert_error| {
@ -653,6 +660,7 @@ pub async fn create_business_profile_from_business_labels(
db, db,
business_profile_create_request, business_profile_create_request,
merchant_account.clone(), merchant_account.clone(),
key_store,
) )
.await .await
.map_err(|business_profile_insert_error| { .map_err(|business_profile_insert_error| {
@ -723,6 +731,7 @@ pub async fn update_business_profile_cascade(
collect_shipping_details_from_wallet_connector: None, collect_shipping_details_from_wallet_connector: None,
collect_billing_details_from_wallet_connector: None, collect_billing_details_from_wallet_connector: None,
is_connector_agnostic_mit_enabled: None, is_connector_agnostic_mit_enabled: None,
outgoing_webhook_custom_http_headers: None,
}; };
let update_futures = business_profiles.iter().map(|business_profile| async { let update_futures = business_profiles.iter().map(|business_profile| async {
@ -1823,11 +1832,10 @@ pub async fn create_and_insert_business_profile(
db: &dyn StorageInterface, db: &dyn StorageInterface,
request: api::BusinessProfileCreate, request: api::BusinessProfileCreate,
merchant_account: domain::MerchantAccount, merchant_account: domain::MerchantAccount,
key_store: &domain::MerchantKeyStore,
) -> RouterResult<storage::business_profile::BusinessProfile> { ) -> RouterResult<storage::business_profile::BusinessProfile> {
let business_profile_new = storage::business_profile::BusinessProfileNew::foreign_try_from(( let business_profile_new =
merchant_account, admin::create_business_profile(merchant_account, request, key_store).await?;
request,
))?;
let profile_name = business_profile_new.profile_name.clone(); let profile_name = business_profile_new.profile_name.clone();
@ -1878,7 +1886,8 @@ pub async fn create_business_profile(
} }
let business_profile = let business_profile =
create_and_insert_business_profile(db, request, merchant_account.clone()).await?; create_and_insert_business_profile(db, request, merchant_account.clone(), &key_store)
.await?;
if merchant_account.default_profile.is_some() { if merchant_account.default_profile.is_some() {
let unset_default_profile = domain::MerchantAccountUpdate::UnsetDefaultProfile; let unset_default_profile = domain::MerchantAccountUpdate::UnsetDefaultProfile;
@ -1888,8 +1897,10 @@ pub async fn create_business_profile(
} }
Ok(service_api::ApplicationResponse::Json( Ok(service_api::ApplicationResponse::Json(
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) admin::business_profile_response(business_profile, &key_store)
.change_context(errors::ApiErrorResponse::InternalServerError)?, .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse business profile details")
.await?,
)) ))
} }
@ -1898,17 +1909,23 @@ pub async fn list_business_profile(
merchant_id: String, merchant_id: String,
) -> RouterResponse<Vec<api_models::admin::BusinessProfileResponse>> { ) -> RouterResponse<Vec<api_models::admin::BusinessProfileResponse>> {
let db = state.store.as_ref(); let db = state.store.as_ref();
let business_profiles = db let key_store = db
.get_merchant_key_store_by_merchant_id(&merchant_id, &db.get_master_key().to_vec().into())
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let profiles = db
.list_business_profile_by_merchant_id(&merchant_id) .list_business_profile_by_merchant_id(&merchant_id)
.await .await
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)? .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?
.into_iter() .clone();
.map(|business_profile| { let mut business_profiles = Vec::new();
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) for profile in profiles {
}) let business_profile = admin::business_profile_response(profile, &key_store)
.collect::<Result<Vec<_>, _>>() .await
.change_context(errors::ApiErrorResponse::InternalServerError) .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse business profile details")?; .attach_printable("Failed to parse business profile details")?;
business_profiles.push(business_profile);
}
Ok(service_api::ApplicationResponse::Json(business_profiles)) Ok(service_api::ApplicationResponse::Json(business_profiles))
} }
@ -1916,8 +1933,13 @@ pub async fn list_business_profile(
pub async fn retrieve_business_profile( pub async fn retrieve_business_profile(
state: SessionState, state: SessionState,
profile_id: String, profile_id: String,
merchant_id: String,
) -> RouterResponse<api_models::admin::BusinessProfileResponse> { ) -> RouterResponse<api_models::admin::BusinessProfileResponse> {
let db = state.store.as_ref(); let db = state.store.as_ref();
let key_store = db
.get_merchant_key_store_by_merchant_id(&merchant_id, &db.get_master_key().to_vec().into())
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?;
let business_profile = db let business_profile = db
.find_business_profile_by_profile_id(&profile_id) .find_business_profile_by_profile_id(&profile_id)
.await .await
@ -1926,8 +1948,10 @@ pub async fn retrieve_business_profile(
})?; })?;
Ok(service_api::ApplicationResponse::Json( Ok(service_api::ApplicationResponse::Json(
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) admin::business_profile_response(business_profile, &key_store)
.change_context(errors::ApiErrorResponse::InternalServerError)?, .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse business profile details")
.await?,
)) ))
} }
@ -1960,6 +1984,14 @@ pub async fn update_business_profile(
.to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound {
id: profile_id.to_owned(), id: profile_id.to_owned(),
})?; })?;
let key_store = db
.get_merchant_key_store_by_merchant_id(
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
.attach_printable("Error while fetching the key store by merchant_id")?;
if business_profile.merchant_id != merchant_id { if business_profile.merchant_id != merchant_id {
Err(errors::ApiErrorResponse::AccessForbidden { Err(errors::ApiErrorResponse::AccessForbidden {
@ -2021,6 +2053,13 @@ pub async fn update_business_profile(
}) })
.transpose()? .transpose()?
.map(Secret::new); .map(Secret::new);
let outgoing_webhook_custom_http_headers = request
.outgoing_webhook_custom_http_headers
.async_map(|headers| create_encrypted_data(&key_store, headers))
.await
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encrypt outgoing webhook custom HTTP headers")?;
let business_profile_update = storage::business_profile::BusinessProfileUpdate::Update { let business_profile_update = storage::business_profile::BusinessProfileUpdate::Update {
profile_name: request.profile_name, profile_name: request.profile_name,
@ -2065,6 +2104,7 @@ pub async fn update_business_profile(
collect_billing_details_from_wallet_connector: request collect_billing_details_from_wallet_connector: request
.collect_billing_details_from_wallet_connector, .collect_billing_details_from_wallet_connector,
is_connector_agnostic_mit_enabled: request.is_connector_agnostic_mit_enabled, is_connector_agnostic_mit_enabled: request.is_connector_agnostic_mit_enabled,
outgoing_webhook_custom_http_headers: outgoing_webhook_custom_http_headers.map(Into::into),
}; };
let updated_business_profile = db let updated_business_profile = db
@ -2075,8 +2115,10 @@ pub async fn update_business_profile(
})?; })?;
Ok(service_api::ApplicationResponse::Json( Ok(service_api::ApplicationResponse::Json(
api_models::admin::BusinessProfileResponse::foreign_try_from(updated_business_profile) admin::business_profile_response(updated_business_profile, &key_store)
.change_context(errors::ApiErrorResponse::InternalServerError)?, .change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Failed to parse business profile details")
.await?,
)) ))
} }

View File

@ -287,6 +287,7 @@ pub async fn update_business_profile_active_algorithm_ref(
collect_shipping_details_from_wallet_connector: None, collect_shipping_details_from_wallet_connector: None,
collect_billing_details_from_wallet_connector: None, collect_billing_details_from_wallet_connector: None,
is_connector_agnostic_mit_enabled: None, is_connector_agnostic_mit_enabled: None,
outgoing_webhook_custom_http_headers: None,
}; };
db.update_business_profile_by_profile_id(current_business_profile, business_profile_update) db.update_business_profile_by_profile_id(current_business_profile, business_profile_update)

View File

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

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use api_models::{ use api_models::{
webhook_events::{OutgoingWebhookRequestContent, OutgoingWebhookResponseContent}, webhook_events::{OutgoingWebhookRequestContent, OutgoingWebhookResponseContent},
webhooks, webhooks,
@ -5,6 +7,7 @@ use api_models::{
use common_utils::{ext_traits::Encode, request::RequestContent}; use common_utils::{ext_traits::Encode, request::RequestContent};
use diesel_models::process_tracker::business_status; use diesel_models::process_tracker::business_status;
use error_stack::{report, ResultExt}; use error_stack::{report, ResultExt};
use hyperswitch_domain_models::type_encryption::decrypt;
use masking::{ExposeInterface, Mask, PeekInterface, Secret}; use masking::{ExposeInterface, Mask, PeekInterface, Secret};
use router_env::{ use router_env::{
instrument, instrument,
@ -86,8 +89,10 @@ pub(crate) async fn create_event_and_trigger_outgoing_webhook(
let request_content = get_outgoing_webhook_request( let request_content = get_outgoing_webhook_request(
&merchant_account, &merchant_account,
outgoing_webhook, outgoing_webhook,
business_profile.payment_response_hash_key.as_deref(), &business_profile,
merchant_key_store,
) )
.await
.change_context(errors::ApiErrorResponse::WebhookProcessingFailure) .change_context(errors::ApiErrorResponse::WebhookProcessingFailure)
.attach_printable("Failed to construct outgoing webhook request content")?; .attach_printable("Failed to construct outgoing webhook request content")?;
@ -546,15 +551,17 @@ fn get_webhook_url_from_business_profile(
.map(ExposeInterface::expose) .map(ExposeInterface::expose)
} }
pub(crate) fn get_outgoing_webhook_request( pub(crate) async fn get_outgoing_webhook_request(
merchant_account: &domain::MerchantAccount, merchant_account: &domain::MerchantAccount,
outgoing_webhook: api::OutgoingWebhook, 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> { ) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
#[inline] #[inline]
fn get_outgoing_webhook_request_inner<WebhookType: types::OutgoingWebhookType>( async fn get_outgoing_webhook_request_inner<WebhookType: types::OutgoingWebhookType>(
outgoing_webhook: api::OutgoingWebhook, 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> { ) -> CustomResult<OutgoingWebhookRequestContent, errors::WebhooksFlowError> {
let mut headers = vec![( let mut headers = vec![(
reqwest::header::CONTENT_TYPE.to_string(), 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 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 let outgoing_webhooks_signature = transformed_outgoing_webhook
.get_outgoing_webhooks_signature(payment_response_hash_key)?; .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() { match merchant_account.get_compatible_connector() {
#[cfg(feature = "stripe")] #[cfg(feature = "stripe")]
Some(api_models::enums::Connector::Stripe) => get_outgoing_webhook_request_inner::< Some(api_models::enums::Connector::Stripe) => {
stripe_webhooks::StripeOutgoingWebhook, get_outgoing_webhook_request_inner::<stripe_webhooks::StripeOutgoingWebhook>(
>(
outgoing_webhook, payment_response_hash_key
),
_ => get_outgoing_webhook_request_inner::<webhooks::OutgoingWebhook>(
outgoing_webhook, outgoing_webhook,
payment_response_hash_key, business_profile,
), key_store,
)
.await
}
_ => {
get_outgoing_webhook_request_inner::<webhooks::OutgoingWebhook>(
outgoing_webhook,
business_profile,
key_store,
)
.await
}
} }
} }

View File

@ -508,22 +508,22 @@ pub async fn business_profile_retrieve(
let flow = Flow::BusinessProfileRetrieve; let flow = Flow::BusinessProfileRetrieve;
let (merchant_id, profile_id) = path.into_inner(); let (merchant_id, profile_id) = path.into_inner();
api::server_wrap( Box::pin(api::server_wrap(
flow, flow,
state, state,
&req, &req,
profile_id, profile_id,
|state, _, profile_id, _| retrieve_business_profile(state, profile_id), |state, _, profile_id, _| retrieve_business_profile(state, profile_id, merchant_id.clone()),
auth::auth_type( auth::auth_type(
&auth::AdminApiAuth, &auth::AdminApiAuth,
&auth::JWTAuthMerchantFromRoute { &auth::JWTAuthMerchantFromRoute {
merchant_id, merchant_id: merchant_id.clone(),
required_permission: Permission::MerchantAccountRead, required_permission: Permission::MerchantAccountRead,
}, },
req.headers(), req.headers(),
), ),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
) ))
.await .await
} }
#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileUpdate))] #[instrument(skip_all, fields(flow = ?Flow::BusinessProfileUpdate))]
@ -583,7 +583,7 @@ pub async fn business_profiles_list(
let flow = Flow::BusinessProfileList; let flow = Flow::BusinessProfileList;
let merchant_id = path.into_inner(); let merchant_id = path.into_inner();
api::server_wrap( Box::pin(api::server_wrap(
flow, flow,
state, state,
&req, &req,
@ -598,7 +598,7 @@ pub async fn business_profiles_list(
req.headers(), req.headers(),
), ),
api_locking::LockAction::NotApplicable, api_locking::LockAction::NotApplicable,
) ))
.await .await
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
pub use api_models::admin::{ pub use api_models::admin::{
BusinessProfileCreate, BusinessProfileResponse, BusinessProfileUpdate, MerchantAccountCreate, BusinessProfileCreate, BusinessProfileResponse, BusinessProfileUpdate, MerchantAccountCreate,
MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate, MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate,
@ -6,12 +8,13 @@ pub use api_models::admin::{
MerchantId, PaymentMethodsEnabled, ToggleAllKVRequest, ToggleAllKVResponse, ToggleKVRequest, MerchantId, PaymentMethodsEnabled, ToggleAllKVRequest, ToggleAllKVResponse, ToggleKVRequest,
ToggleKVResponse, WebhookDetails, ToggleKVResponse, WebhookDetails,
}; };
use common_utils::ext_traits::{Encode, ValueExt}; use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
use error_stack::ResultExt; use error_stack::ResultExt;
use masking::{ExposeInterface, Secret}; use hyperswitch_domain_models::{merchant_key_store::MerchantKeyStore, type_encryption::decrypt};
use masking::{ExposeInterface, PeekInterface, Secret};
use crate::{ use crate::{
core::errors, core::{errors, payment_methods::cards::create_encrypted_data},
types::{domain, storage, transformers::ForeignTryFrom}, types::{domain, storage, transformers::ForeignTryFrom},
}; };
@ -80,13 +83,28 @@ impl ForeignTryFrom<domain::MerchantAccount> for MerchantAccountResponse {
} }
} }
impl ForeignTryFrom<storage::business_profile::BusinessProfile> for BusinessProfileResponse { pub async fn business_profile_response(
type Error = error_stack::Report<errors::ParsingError>;
fn foreign_try_from(
item: storage::business_profile::BusinessProfile, item: storage::business_profile::BusinessProfile,
) -> Result<Self, Self::Error> { key_store: &MerchantKeyStore,
Ok(Self { ) -> Result<BusinessProfileResponse, error_stack::Report<errors::ParsingError>> {
let outgoing_webhook_custom_http_headers = decrypt::<serde_json::Value, masking::WithType>(
item.outgoing_webhook_custom_http_headers.clone(),
key_store.key.get_inner().peek(),
)
.await
.change_context(errors::ParsingError::StructParseFailure(
"Outgoing webhook custom HTTP headers",
))
.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>")
})
.transpose()?;
Ok(BusinessProfileResponse {
merchant_id: item.merchant_id, merchant_id: item.merchant_id,
profile_id: item.profile_id, profile_id: item.profile_id,
profile_name: item.profile_name, profile_name: item.profile_name,
@ -112,9 +130,7 @@ impl ForeignTryFrom<storage::business_profile::BusinessProfile> for BusinessProf
.transpose()?, .transpose()?,
payout_link_config: item payout_link_config: item
.payout_link_config .payout_link_config
.map(|payout_link_config| { .map(|payout_link_config| payout_link_config.parse_value("BusinessPayoutLinkConfig"))
payout_link_config.parse_value("BusinessPayoutLinkConfig")
})
.transpose()?, .transpose()?,
use_billing_as_payment_method_billing: item.use_billing_as_payment_method_billing, use_billing_as_payment_method_billing: item.use_billing_as_payment_method_billing,
extended_card_info_config: item extended_card_info_config: item
@ -126,18 +142,18 @@ impl ForeignTryFrom<storage::business_profile::BusinessProfile> for BusinessProf
collect_billing_details_from_wallet_connector: item collect_billing_details_from_wallet_connector: item
.collect_billing_details_from_wallet_connector, .collect_billing_details_from_wallet_connector,
is_connector_agnostic_mit_enabled: item.is_connector_agnostic_mit_enabled, is_connector_agnostic_mit_enabled: item.is_connector_agnostic_mit_enabled,
outgoing_webhook_custom_http_headers,
}) })
} }
}
impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> pub async fn create_business_profile(
for storage::business_profile::BusinessProfileNew merchant_account: domain::MerchantAccount,
{ request: BusinessProfileCreate,
type Error = error_stack::Report<errors::ApiErrorResponse>; key_store: &MerchantKeyStore,
) -> Result<
fn foreign_try_from( storage::business_profile::BusinessProfileNew,
(merchant_account, request): (domain::MerchantAccount, BusinessProfileCreate), error_stack::Report<errors::ApiErrorResponse>,
) -> Result<Self, Self::Error> { > {
// Generate a unique profile id // Generate a unique profile id
let profile_id = common_utils::generate_id_with_default_len("pro"); let profile_id = common_utils::generate_id_with_default_len("pro");
@ -163,15 +179,22 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)>
let payment_link_config_value = request let payment_link_config_value = request
.payment_link_config .payment_link_config
.map(|pl_config| { .map(|pl_config| {
pl_config.encode_to_value().change_context( pl_config
errors::ApiErrorResponse::InvalidDataValue { .encode_to_value()
.change_context(errors::ApiErrorResponse::InvalidDataValue {
field_name: "payment_link_config_value", field_name: "payment_link_config_value",
}, })
)
}) })
.transpose()?; .transpose()?;
let outgoing_webhook_custom_http_headers = request
.outgoing_webhook_custom_http_headers
.async_map(|headers| create_encrypted_data(key_store, headers))
.await
.transpose()
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable("Unable to encrypt outgoing webhook custom HTTP headers")?;
Ok(Self { Ok(storage::business_profile::BusinessProfileNew {
profile_id, profile_id,
merchant_id: merchant_account.merchant_id, merchant_id: merchant_account.merchant_id,
profile_name: request.profile_name.unwrap_or("default".to_string()), profile_name: request.profile_name.unwrap_or("default".to_string()),
@ -243,6 +266,6 @@ impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)>
collect_billing_details_from_wallet_connector: request collect_billing_details_from_wallet_connector: request
.collect_billing_details_from_wallet_connector .collect_billing_details_from_wallet_connector
.or(Some(false)), .or(Some(false)),
outgoing_webhook_custom_http_headers: outgoing_webhook_custom_http_headers.map(Into::into),
}) })
} }
}

View File

@ -164,8 +164,10 @@ impl ProcessTrackerWorkflow<SessionState> for OutgoingWebhookRetryWorkflow {
let request_content = webhooks_core::get_outgoing_webhook_request( let request_content = webhooks_core::get_outgoing_webhook_request(
&merchant_account, &merchant_account,
outgoing_webhook, outgoing_webhook,
business_profile.payment_response_hash_key.as_deref(), &business_profile,
&key_store,
) )
.await
.map_err(|error| { .map_err(|error| {
logger::error!( logger::error!(
?error, ?error,

View File

@ -0,0 +1 @@
ALTER TABLE business_profile DROP COLUMN IF EXISTS outgoing_webhook_custom_http_headers;

View File

@ -0,0 +1 @@
ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS outgoing_webhook_custom_http_headers BYTEA DEFAULT NULL;