mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-01 02:57:02 +08:00
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:
@ -7006,6 +7006,11 @@
|
||||
}
|
||||
],
|
||||
"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
|
||||
@ -7164,6 +7169,11 @@
|
||||
}
|
||||
],
|
||||
"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
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1180,6 +1180,10 @@ pub struct BusinessProfileCreate {
|
||||
/// Default payout link config
|
||||
#[schema(value_type = 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)]
|
||||
@ -1272,6 +1276,10 @@ pub struct BusinessProfileResponse {
|
||||
/// Default payout link config
|
||||
#[schema(value_type = 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)]
|
||||
@ -1356,6 +1364,10 @@ pub struct BusinessProfileUpdate {
|
||||
/// Default payout link config
|
||||
#[schema(value_type = 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)]
|
||||
pub struct BusinessCollectLinkConfig {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use common_utils::pii;
|
||||
use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable};
|
||||
|
||||
use crate::schema::business_profile;
|
||||
use crate::{encryption::Encryption, schema::business_profile};
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
@ -43,6 +43,7 @@ pub struct BusinessProfile {
|
||||
pub use_billing_as_payment_method_billing: Option<bool>,
|
||||
pub collect_shipping_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)]
|
||||
@ -76,6 +77,7 @@ pub struct BusinessProfileNew {
|
||||
pub use_billing_as_payment_method_billing: Option<bool>,
|
||||
pub collect_shipping_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)]
|
||||
@ -106,6 +108,7 @@ pub struct BusinessProfileUpdateInternal {
|
||||
pub use_billing_as_payment_method_billing: Option<bool>,
|
||||
pub collect_shipping_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)]
|
||||
@ -134,6 +137,7 @@ pub enum BusinessProfileUpdate {
|
||||
collect_shipping_details_from_wallet_connector: Option<bool>,
|
||||
collect_billing_details_from_wallet_connector: Option<bool>,
|
||||
is_connector_agnostic_mit_enabled: Option<bool>,
|
||||
outgoing_webhook_custom_http_headers: Option<Encryption>,
|
||||
},
|
||||
ExtendedCardInfoUpdate {
|
||||
is_extended_card_info_enabled: Option<bool>,
|
||||
@ -170,6 +174,7 @@ impl From<BusinessProfileUpdate> for BusinessProfileUpdateInternal {
|
||||
collect_shipping_details_from_wallet_connector,
|
||||
collect_billing_details_from_wallet_connector,
|
||||
is_connector_agnostic_mit_enabled,
|
||||
outgoing_webhook_custom_http_headers,
|
||||
} => Self {
|
||||
profile_name,
|
||||
modified_at,
|
||||
@ -194,6 +199,7 @@ impl From<BusinessProfileUpdate> for BusinessProfileUpdateInternal {
|
||||
collect_shipping_details_from_wallet_connector,
|
||||
collect_billing_details_from_wallet_connector,
|
||||
is_connector_agnostic_mit_enabled,
|
||||
outgoing_webhook_custom_http_headers,
|
||||
..Default::default()
|
||||
},
|
||||
BusinessProfileUpdate::ExtendedCardInfoUpdate {
|
||||
@ -244,6 +250,7 @@ impl From<BusinessProfileNew> for BusinessProfile {
|
||||
.collect_shipping_details_from_wallet_connector,
|
||||
collect_billing_details_from_wallet_connector: new
|
||||
.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,
|
||||
collect_shipping_details_from_wallet_connector,
|
||||
collect_billing_details_from_wallet_connector,
|
||||
outgoing_webhook_custom_http_headers,
|
||||
} = self.into();
|
||||
BusinessProfile {
|
||||
profile_name: profile_name.unwrap_or(source.profile_name),
|
||||
@ -303,6 +311,7 @@ impl BusinessProfileUpdate {
|
||||
use_billing_as_payment_method_billing,
|
||||
collect_shipping_details_from_wallet_connector,
|
||||
collect_billing_details_from_wallet_connector,
|
||||
outgoing_webhook_custom_http_headers,
|
||||
..source
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,6 +204,7 @@ diesel::table! {
|
||||
use_billing_as_payment_method_billing -> Nullable<Bool>,
|
||||
collect_shipping_details_from_wallet_connector -> Nullable<Bool>,
|
||||
collect_billing_details_from_wallet_connector -> Nullable<Bool>,
|
||||
outgoing_webhook_custom_http_headers -> Nullable<Bytea>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -204,6 +204,7 @@ diesel::table! {
|
||||
use_billing_as_payment_method_billing -> Nullable<Bool>,
|
||||
collect_shipping_details_from_wallet_connector -> Nullable<Bool>,
|
||||
collect_billing_details_from_wallet_connector -> Nullable<Bool>,
|
||||
outgoing_webhook_custom_http_headers -> Nullable<Bytea>,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -25,7 +25,7 @@ use crate::{
|
||||
core::{
|
||||
encryption::transfer_encryption_key,
|
||||
errors::{self, RouterResponse, RouterResult, StorageErrorExt},
|
||||
payment_methods::{cards, transformers},
|
||||
payment_methods::{cards, cards::create_encrypted_data, transformers},
|
||||
payments::helpers,
|
||||
pm_auth::helpers::PaymentAuthConnectorDataExt,
|
||||
routing::helpers as routing_helpers,
|
||||
@ -35,7 +35,8 @@ use crate::{
|
||||
routes::{metrics, SessionState},
|
||||
services::{self, api as service_api, authentication, pm_auth as payment_initiation_service},
|
||||
types::{
|
||||
self, api,
|
||||
self,
|
||||
api::{self, admin},
|
||||
domain::{
|
||||
self,
|
||||
types::{self as domain_types, AsyncLift},
|
||||
@ -239,7 +240,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
|
||||
.create_or_validate(db)
|
||||
.await?;
|
||||
|
||||
let key = key_store.key.into_inner();
|
||||
let key = key_store.key.clone().into_inner();
|
||||
|
||||
let mut merchant_account = async {
|
||||
Ok::<_, error_stack::Report<common_utils::errors::CryptoError>>(
|
||||
@ -292,7 +293,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate {
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?;
|
||||
|
||||
CreateBusinessProfile::new(self.primary_business_details.clone())
|
||||
.create_business_profiles(db, &mut merchant_account)
|
||||
.create_business_profiles(db, &mut merchant_account, &key_store)
|
||||
.await?;
|
||||
|
||||
Ok(merchant_account)
|
||||
@ -386,6 +387,7 @@ impl CreateBusinessProfile {
|
||||
&self,
|
||||
db: &dyn StorageInterface,
|
||||
merchant_account: &mut domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<()> {
|
||||
match self {
|
||||
Self::CreateFromPrimaryBusinessDetails {
|
||||
@ -395,6 +397,7 @@ impl CreateBusinessProfile {
|
||||
db,
|
||||
merchant_account.clone(),
|
||||
primary_business_details,
|
||||
key_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -407,7 +410,7 @@ impl CreateBusinessProfile {
|
||||
}
|
||||
Self::CreateDefaultBusinessProfile => {
|
||||
let business_profile = self
|
||||
.create_default_business_profile(db, merchant_account.clone())
|
||||
.create_default_business_profile(db, merchant_account.clone(), key_store)
|
||||
.await?;
|
||||
|
||||
merchant_account.default_profile = Some(business_profile.profile_id);
|
||||
@ -422,11 +425,13 @@ impl CreateBusinessProfile {
|
||||
&self,
|
||||
db: &dyn StorageInterface,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<diesel_models::business_profile::BusinessProfile> {
|
||||
let business_profile = create_and_insert_business_profile(
|
||||
db,
|
||||
api_models::admin::BusinessProfileCreate::default(),
|
||||
merchant_account.clone(),
|
||||
key_store,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@ -440,6 +445,7 @@ impl CreateBusinessProfile {
|
||||
db: &dyn StorageInterface,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
primary_business_details: &Vec<admin_types::PrimaryBusinessDetails>,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<Vec<diesel_models::business_profile::BusinessProfile>> {
|
||||
let mut business_profiles_vector = Vec::with_capacity(primary_business_details.len());
|
||||
|
||||
@ -459,6 +465,7 @@ impl CreateBusinessProfile {
|
||||
db,
|
||||
business_profile_create_request,
|
||||
merchant_account.clone(),
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
.map_err(|business_profile_insert_error| {
|
||||
@ -653,6 +660,7 @@ pub async fn create_business_profile_from_business_labels(
|
||||
db,
|
||||
business_profile_create_request,
|
||||
merchant_account.clone(),
|
||||
key_store,
|
||||
)
|
||||
.await
|
||||
.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_billing_details_from_wallet_connector: None,
|
||||
is_connector_agnostic_mit_enabled: None,
|
||||
outgoing_webhook_custom_http_headers: None,
|
||||
};
|
||||
|
||||
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,
|
||||
request: api::BusinessProfileCreate,
|
||||
merchant_account: domain::MerchantAccount,
|
||||
key_store: &domain::MerchantKeyStore,
|
||||
) -> RouterResult<storage::business_profile::BusinessProfile> {
|
||||
let business_profile_new = storage::business_profile::BusinessProfileNew::foreign_try_from((
|
||||
merchant_account,
|
||||
request,
|
||||
))?;
|
||||
let business_profile_new =
|
||||
admin::create_business_profile(merchant_account, request, key_store).await?;
|
||||
|
||||
let profile_name = business_profile_new.profile_name.clone();
|
||||
|
||||
@ -1878,7 +1886,8 @@ pub async fn create_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() {
|
||||
let unset_default_profile = domain::MerchantAccountUpdate::UnsetDefaultProfile;
|
||||
@ -1888,8 +1897,10 @@ pub async fn create_business_profile(
|
||||
}
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(
|
||||
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?,
|
||||
admin::business_profile_response(business_profile, &key_store)
|
||||
.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,
|
||||
) -> RouterResponse<Vec<api_models::admin::BusinessProfileResponse>> {
|
||||
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)
|
||||
.await
|
||||
.to_not_found_response(errors::ApiErrorResponse::InternalServerError)?
|
||||
.into_iter()
|
||||
.map(|business_profile| {
|
||||
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile)
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.clone();
|
||||
let mut business_profiles = Vec::new();
|
||||
for profile in profiles {
|
||||
let business_profile = admin::business_profile_response(profile, &key_store)
|
||||
.await
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to parse business profile details")?;
|
||||
business_profiles.push(business_profile);
|
||||
}
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(business_profiles))
|
||||
}
|
||||
@ -1916,8 +1933,13 @@ pub async fn list_business_profile(
|
||||
pub async fn retrieve_business_profile(
|
||||
state: SessionState,
|
||||
profile_id: String,
|
||||
merchant_id: String,
|
||||
) -> RouterResponse<api_models::admin::BusinessProfileResponse> {
|
||||
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
|
||||
.find_business_profile_by_profile_id(&profile_id)
|
||||
.await
|
||||
@ -1926,8 +1948,10 @@ pub async fn retrieve_business_profile(
|
||||
})?;
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(
|
||||
api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?,
|
||||
admin::business_profile_response(business_profile, &key_store)
|
||||
.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 {
|
||||
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 {
|
||||
Err(errors::ApiErrorResponse::AccessForbidden {
|
||||
@ -2021,6 +2053,13 @@ pub async fn update_business_profile(
|
||||
})
|
||||
.transpose()?
|
||||
.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 {
|
||||
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,
|
||||
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
|
||||
@ -2075,8 +2115,10 @@ pub async fn update_business_profile(
|
||||
})?;
|
||||
|
||||
Ok(service_api::ApplicationResponse::Json(
|
||||
api_models::admin::BusinessProfileResponse::foreign_try_from(updated_business_profile)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)?,
|
||||
admin::business_profile_response(updated_business_profile, &key_store)
|
||||
.change_context(errors::ApiErrorResponse::InternalServerError)
|
||||
.attach_printable("Failed to parse business profile details")
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
@ -287,6 +287,7 @@ pub async fn update_business_profile_active_algorithm_ref(
|
||||
collect_shipping_details_from_wallet_connector: None,
|
||||
collect_billing_details_from_wallet_connector: 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)
|
||||
|
||||
@ -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?;
|
||||
}
|
||||
|
||||
|
||||
@ -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>(
|
||||
Some(api_models::enums::Connector::Stripe) => {
|
||||
get_outgoing_webhook_request_inner::<stripe_webhooks::StripeOutgoingWebhook>(
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -508,22 +508,22 @@ pub async fn business_profile_retrieve(
|
||||
let flow = Flow::BusinessProfileRetrieve;
|
||||
let (merchant_id, profile_id) = path.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
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::AdminApiAuth,
|
||||
&auth::JWTAuthMerchantFromRoute {
|
||||
merchant_id,
|
||||
merchant_id: merchant_id.clone(),
|
||||
required_permission: Permission::MerchantAccountRead,
|
||||
},
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
))
|
||||
.await
|
||||
}
|
||||
#[instrument(skip_all, fields(flow = ?Flow::BusinessProfileUpdate))]
|
||||
@ -583,7 +583,7 @@ pub async fn business_profiles_list(
|
||||
let flow = Flow::BusinessProfileList;
|
||||
let merchant_id = path.into_inner();
|
||||
|
||||
api::server_wrap(
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state,
|
||||
&req,
|
||||
@ -598,7 +598,7 @@ pub async fn business_profiles_list(
|
||||
req.headers(),
|
||||
),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
)
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub use api_models::admin::{
|
||||
BusinessProfileCreate, BusinessProfileResponse, BusinessProfileUpdate, MerchantAccountCreate,
|
||||
MerchantAccountDeleteResponse, MerchantAccountResponse, MerchantAccountUpdate,
|
||||
@ -6,12 +8,13 @@ pub use api_models::admin::{
|
||||
MerchantId, PaymentMethodsEnabled, ToggleAllKVRequest, ToggleAllKVResponse, ToggleKVRequest,
|
||||
ToggleKVResponse, WebhookDetails,
|
||||
};
|
||||
use common_utils::ext_traits::{Encode, ValueExt};
|
||||
use common_utils::ext_traits::{AsyncExt, Encode, ValueExt};
|
||||
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::{
|
||||
core::errors,
|
||||
core::{errors, payment_methods::cards::create_encrypted_data},
|
||||
types::{domain, storage, transformers::ForeignTryFrom},
|
||||
};
|
||||
|
||||
@ -80,13 +83,28 @@ impl ForeignTryFrom<domain::MerchantAccount> for MerchantAccountResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignTryFrom<storage::business_profile::BusinessProfile> for BusinessProfileResponse {
|
||||
type Error = error_stack::Report<errors::ParsingError>;
|
||||
|
||||
fn foreign_try_from(
|
||||
pub async fn business_profile_response(
|
||||
item: storage::business_profile::BusinessProfile,
|
||||
) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
key_store: &MerchantKeyStore,
|
||||
) -> 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,
|
||||
profile_id: item.profile_id,
|
||||
profile_name: item.profile_name,
|
||||
@ -112,9 +130,7 @@ impl ForeignTryFrom<storage::business_profile::BusinessProfile> for BusinessProf
|
||||
.transpose()?,
|
||||
payout_link_config: item
|
||||
.payout_link_config
|
||||
.map(|payout_link_config| {
|
||||
payout_link_config.parse_value("BusinessPayoutLinkConfig")
|
||||
})
|
||||
.map(|payout_link_config| payout_link_config.parse_value("BusinessPayoutLinkConfig"))
|
||||
.transpose()?,
|
||||
use_billing_as_payment_method_billing: item.use_billing_as_payment_method_billing,
|
||||
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,
|
||||
is_connector_agnostic_mit_enabled: item.is_connector_agnostic_mit_enabled,
|
||||
outgoing_webhook_custom_http_headers,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)>
|
||||
for storage::business_profile::BusinessProfileNew
|
||||
{
|
||||
type Error = error_stack::Report<errors::ApiErrorResponse>;
|
||||
|
||||
fn foreign_try_from(
|
||||
(merchant_account, request): (domain::MerchantAccount, BusinessProfileCreate),
|
||||
) -> Result<Self, Self::Error> {
|
||||
pub async fn create_business_profile(
|
||||
merchant_account: domain::MerchantAccount,
|
||||
request: BusinessProfileCreate,
|
||||
key_store: &MerchantKeyStore,
|
||||
) -> Result<
|
||||
storage::business_profile::BusinessProfileNew,
|
||||
error_stack::Report<errors::ApiErrorResponse>,
|
||||
> {
|
||||
// Generate a unique profile id
|
||||
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
|
||||
.payment_link_config
|
||||
.map(|pl_config| {
|
||||
pl_config.encode_to_value().change_context(
|
||||
errors::ApiErrorResponse::InvalidDataValue {
|
||||
pl_config
|
||||
.encode_to_value()
|
||||
.change_context(errors::ApiErrorResponse::InvalidDataValue {
|
||||
field_name: "payment_link_config_value",
|
||||
},
|
||||
)
|
||||
})
|
||||
})
|
||||
.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,
|
||||
merchant_id: merchant_account.merchant_id,
|
||||
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
|
||||
.or(Some(false)),
|
||||
outgoing_webhook_custom_http_headers: outgoing_webhook_custom_http_headers.map(Into::into),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -164,8 +164,10 @@ impl ProcessTrackerWorkflow<SessionState> for OutgoingWebhookRetryWorkflow {
|
||||
let request_content = webhooks_core::get_outgoing_webhook_request(
|
||||
&merchant_account,
|
||||
outgoing_webhook,
|
||||
business_profile.payment_response_hash_key.as_deref(),
|
||||
&business_profile,
|
||||
&key_store,
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
logger::error!(
|
||||
?error,
|
||||
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE business_profile DROP COLUMN IF EXISTS outgoing_webhook_custom_http_headers;
|
||||
@ -0,0 +1 @@
|
||||
ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS outgoing_webhook_custom_http_headers BYTEA DEFAULT NULL;
|
||||
Reference in New Issue
Block a user