diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index 2928208e82..e916e6ee46 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -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 } } }, diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 20129e4b23..27bd098953 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -1180,6 +1180,10 @@ pub struct BusinessProfileCreate { /// Default payout link config #[schema(value_type = Option)] pub payout_link_config: Option, + + /// 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, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] + pub outgoing_webhook_custom_http_headers: Option>, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -1272,6 +1276,10 @@ pub struct BusinessProfileResponse { /// Default payout link config #[schema(value_type = Option)] pub payout_link_config: Option, + + /// These key-value pairs are sent as additional custom headers in the outgoing webhook request. + #[schema(value_type = Option, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] + pub outgoing_webhook_custom_http_headers: Option>, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -1356,6 +1364,10 @@ pub struct BusinessProfileUpdate { /// Default payout link config #[schema(value_type = Option)] pub payout_link_config: Option, + + /// 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, example = r#"{ "key1": "value-1", "key2": "value-2" }"#)] + pub outgoing_webhook_custom_http_headers: Option>, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] pub struct BusinessCollectLinkConfig { diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index af34311906..6bd1e1f9e4 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -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, pub collect_shipping_details_from_wallet_connector: Option, pub collect_billing_details_from_wallet_connector: Option, + pub outgoing_webhook_custom_http_headers: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -76,6 +77,7 @@ pub struct BusinessProfileNew { pub use_billing_as_payment_method_billing: Option, pub collect_shipping_details_from_wallet_connector: Option, pub collect_billing_details_from_wallet_connector: Option, + pub outgoing_webhook_custom_http_headers: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -106,6 +108,7 @@ pub struct BusinessProfileUpdateInternal { pub use_billing_as_payment_method_billing: Option, pub collect_shipping_details_from_wallet_connector: Option, pub collect_billing_details_from_wallet_connector: Option, + pub outgoing_webhook_custom_http_headers: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -134,6 +137,7 @@ pub enum BusinessProfileUpdate { collect_shipping_details_from_wallet_connector: Option, collect_billing_details_from_wallet_connector: Option, is_connector_agnostic_mit_enabled: Option, + outgoing_webhook_custom_http_headers: Option, }, ExtendedCardInfoUpdate { is_extended_card_info_enabled: Option, @@ -170,6 +174,7 @@ impl From 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 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 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 } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index d876c2101c..a5185dc607 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -204,6 +204,7 @@ diesel::table! { use_billing_as_payment_method_billing -> Nullable, collect_shipping_details_from_wallet_connector -> Nullable, collect_billing_details_from_wallet_connector -> Nullable, + outgoing_webhook_custom_http_headers -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index cd8ea4e1e8..da8729cf83 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -204,6 +204,7 @@ diesel::table! { use_billing_as_payment_method_billing -> Nullable, collect_shipping_details_from_wallet_connector -> Nullable, collect_billing_details_from_wallet_connector -> Nullable, + outgoing_webhook_custom_http_headers -> Nullable, } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 597b581a1a..d8eb140066 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -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>( @@ -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 { 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, + key_store: &domain::MerchantKeyStore, ) -> RouterResult> { 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 { - 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> { 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::, _>>() - .change_context(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to parse business profile details")?; + .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 { 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?, )) } diff --git a/crates/router/src/core/routing/helpers.rs b/crates/router/src/core/routing/helpers.rs index b9f5da13cf..0e4adbff36 100644 --- a/crates/router/src/core/routing/helpers.rs +++ b/crates/router/src/core/routing/helpers.rs @@ -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) diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 44528e5c68..9eb74dc2df 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -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 = 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 = 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?; } diff --git a/crates/router/src/core/webhooks/outgoing.rs b/crates/router/src/core/webhooks/outgoing.rs index d82e6864bc..c40b4ae886 100644 --- a/crates/router/src/core/webhooks/outgoing.rs +++ b/crates/router/src/core/webhooks/outgoing.rs @@ -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 { #[inline] - fn get_outgoing_webhook_request_inner( + async fn get_outgoing_webhook_request_inner( outgoing_webhook: api::OutgoingWebhook, - payment_response_hash_key: Option<&str>, + business_profile: &diesel_models::business_profile::BusinessProfile, + key_store: &domain::MerchantKeyStore, ) -> CustomResult { 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::( + 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") + .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::( - outgoing_webhook, - payment_response_hash_key, - ), + Some(api_models::enums::Connector::Stripe) => { + get_outgoing_webhook_request_inner::( + outgoing_webhook, + business_profile, + key_store, + ) + .await + } + _ => { + get_outgoing_webhook_request_inner::( + outgoing_webhook, + business_profile, + key_store, + ) + .await + } } } diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index da12c98c77..74bb775d9c 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -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 } diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index 9aa2c95f55..55288e02f8 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -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,169 +83,189 @@ impl ForeignTryFrom for MerchantAccountResponse { } } -impl ForeignTryFrom for BusinessProfileResponse { - type Error = error_stack::Report; +pub async fn business_profile_response( + item: storage::business_profile::BusinessProfile, + key_store: &MerchantKeyStore, +) -> Result> { + let outgoing_webhook_custom_http_headers = decrypt::( + 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") + }) + .transpose()?; - fn foreign_try_from( - item: storage::business_profile::BusinessProfile, - ) -> Result { - Ok(Self { - merchant_id: item.merchant_id, - profile_id: item.profile_id, - profile_name: item.profile_name, - return_url: item.return_url, - enable_payment_response_hash: item.enable_payment_response_hash, - payment_response_hash_key: item.payment_response_hash_key, - redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, - webhook_details: item.webhook_details.map(Secret::new), - metadata: item.metadata, - routing_algorithm: item.routing_algorithm, - intent_fulfillment_time: item.intent_fulfillment_time, - frm_routing_algorithm: item.frm_routing_algorithm, - #[cfg(feature = "payouts")] - payout_routing_algorithm: item.payout_routing_algorithm, - applepay_verified_domains: item.applepay_verified_domains, - payment_link_config: item.payment_link_config, - session_expiry: item.session_expiry, - authentication_connector_details: item - .authentication_connector_details - .map(|authentication_connector_details| { - authentication_connector_details.parse_value("AuthenticationDetails") - }) - .transpose()?, - payout_link_config: item - .payout_link_config - .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 - .extended_card_info_config - .map(|config| config.expose().parse_value("ExtendedCardInfoConfig")) - .transpose()?, - collect_shipping_details_from_wallet_connector: item - .collect_shipping_details_from_wallet_connector, - collect_billing_details_from_wallet_connector: item - .collect_billing_details_from_wallet_connector, - is_connector_agnostic_mit_enabled: item.is_connector_agnostic_mit_enabled, - }) - } + Ok(BusinessProfileResponse { + merchant_id: item.merchant_id, + profile_id: item.profile_id, + profile_name: item.profile_name, + return_url: item.return_url, + enable_payment_response_hash: item.enable_payment_response_hash, + payment_response_hash_key: item.payment_response_hash_key, + redirect_to_merchant_with_http_post: item.redirect_to_merchant_with_http_post, + webhook_details: item.webhook_details.map(Secret::new), + metadata: item.metadata, + routing_algorithm: item.routing_algorithm, + intent_fulfillment_time: item.intent_fulfillment_time, + frm_routing_algorithm: item.frm_routing_algorithm, + #[cfg(feature = "payouts")] + payout_routing_algorithm: item.payout_routing_algorithm, + applepay_verified_domains: item.applepay_verified_domains, + payment_link_config: item.payment_link_config, + session_expiry: item.session_expiry, + authentication_connector_details: item + .authentication_connector_details + .map(|authentication_connector_details| { + authentication_connector_details.parse_value("AuthenticationDetails") + }) + .transpose()?, + payout_link_config: item + .payout_link_config + .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 + .extended_card_info_config + .map(|config| config.expose().parse_value("ExtendedCardInfoConfig")) + .transpose()?, + collect_shipping_details_from_wallet_connector: item + .collect_shipping_details_from_wallet_connector, + 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; +pub async fn create_business_profile( + merchant_account: domain::MerchantAccount, + request: BusinessProfileCreate, + key_store: &MerchantKeyStore, +) -> Result< + storage::business_profile::BusinessProfileNew, + error_stack::Report, +> { + // Generate a unique profile id + let profile_id = common_utils::generate_id_with_default_len("pro"); - fn foreign_try_from( - (merchant_account, request): (domain::MerchantAccount, BusinessProfileCreate), - ) -> Result { - // Generate a unique profile id - let profile_id = common_utils::generate_id_with_default_len("pro"); + let current_time = common_utils::date_time::now(); - let current_time = common_utils::date_time::now(); + let webhook_details = request + .webhook_details + .as_ref() + .map(|webhook_details| { + webhook_details.encode_to_value().change_context( + errors::ApiErrorResponse::InvalidDataValue { + field_name: "webhook details", + }, + ) + }) + .transpose()?; - let webhook_details = request - .webhook_details + let payment_response_hash_key = request + .payment_response_hash_key + .or(merchant_account.payment_response_hash_key) + .unwrap_or(common_utils::crypto::generate_cryptographically_secure_random_string(64)); + + let payment_link_config_value = request + .payment_link_config + .map(|pl_config| { + 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(storage::business_profile::BusinessProfileNew { + profile_id, + merchant_id: merchant_account.merchant_id, + profile_name: request.profile_name.unwrap_or("default".to_string()), + created_at: current_time, + modified_at: current_time, + return_url: request + .return_url + .map(|return_url| return_url.to_string()) + .or(merchant_account.return_url), + enable_payment_response_hash: request + .enable_payment_response_hash + .unwrap_or(merchant_account.enable_payment_response_hash), + payment_response_hash_key: Some(payment_response_hash_key), + redirect_to_merchant_with_http_post: request + .redirect_to_merchant_with_http_post + .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), + webhook_details: webhook_details.or(merchant_account.webhook_details), + metadata: request.metadata, + routing_algorithm: Some(serde_json::json!({ + "algorithm_id": null, + "timestamp": 0 + })), + intent_fulfillment_time: request + .intent_fulfillment_time + .map(i64::from) + .or(merchant_account.intent_fulfillment_time) + .or(Some(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME)), + frm_routing_algorithm: request + .frm_routing_algorithm + .or(merchant_account.frm_routing_algorithm), + #[cfg(feature = "payouts")] + payout_routing_algorithm: request + .payout_routing_algorithm + .or(merchant_account.payout_routing_algorithm), + #[cfg(not(feature = "payouts"))] + payout_routing_algorithm: None, + is_recon_enabled: merchant_account.is_recon_enabled, + applepay_verified_domains: request.applepay_verified_domains, + payment_link_config: payment_link_config_value, + session_expiry: request + .session_expiry + .map(i64::from) + .or(Some(common_utils::consts::DEFAULT_SESSION_EXPIRY)), + authentication_connector_details: request + .authentication_connector_details .as_ref() - .map(|webhook_details| { - webhook_details.encode_to_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "webhook details", - }, - ) - }) - .transpose()?; - - let payment_response_hash_key = request - .payment_response_hash_key - .or(merchant_account.payment_response_hash_key) - .unwrap_or(common_utils::crypto::generate_cryptographically_secure_random_string(64)); - - let payment_link_config_value = request - .payment_link_config - .map(|pl_config| { - pl_config.encode_to_value().change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "payment_link_config_value", - }, - ) - }) - .transpose()?; - - Ok(Self { - profile_id, - merchant_id: merchant_account.merchant_id, - profile_name: request.profile_name.unwrap_or("default".to_string()), - created_at: current_time, - modified_at: current_time, - return_url: request - .return_url - .map(|return_url| return_url.to_string()) - .or(merchant_account.return_url), - enable_payment_response_hash: request - .enable_payment_response_hash - .unwrap_or(merchant_account.enable_payment_response_hash), - payment_response_hash_key: Some(payment_response_hash_key), - redirect_to_merchant_with_http_post: request - .redirect_to_merchant_with_http_post - .unwrap_or(merchant_account.redirect_to_merchant_with_http_post), - webhook_details: webhook_details.or(merchant_account.webhook_details), - metadata: request.metadata, - routing_algorithm: Some(serde_json::json!({ - "algorithm_id": null, - "timestamp": 0 - })), - intent_fulfillment_time: request - .intent_fulfillment_time - .map(i64::from) - .or(merchant_account.intent_fulfillment_time) - .or(Some(common_utils::consts::DEFAULT_INTENT_FULFILLMENT_TIME)), - frm_routing_algorithm: request - .frm_routing_algorithm - .or(merchant_account.frm_routing_algorithm), - #[cfg(feature = "payouts")] - payout_routing_algorithm: request - .payout_routing_algorithm - .or(merchant_account.payout_routing_algorithm), - #[cfg(not(feature = "payouts"))] - payout_routing_algorithm: None, - is_recon_enabled: merchant_account.is_recon_enabled, - applepay_verified_domains: request.applepay_verified_domains, - payment_link_config: payment_link_config_value, - session_expiry: request - .session_expiry - .map(i64::from) - .or(Some(common_utils::consts::DEFAULT_SESSION_EXPIRY)), - authentication_connector_details: request - .authentication_connector_details - .as_ref() - .map(Encode::encode_to_value) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "authentication_connector_details", - })?, - payout_link_config: request - .payout_link_config - .as_ref() - .map(Encode::encode_to_value) - .transpose() - .change_context(errors::ApiErrorResponse::InvalidDataValue { - field_name: "payout_link_config", - })?, - is_connector_agnostic_mit_enabled: request.is_connector_agnostic_mit_enabled, - is_extended_card_info_enabled: None, - extended_card_info_config: None, - use_billing_as_payment_method_billing: request - .use_billing_as_payment_method_billing - .or(Some(true)), - collect_shipping_details_from_wallet_connector: request - .collect_shipping_details_from_wallet_connector - .or(Some(false)), - collect_billing_details_from_wallet_connector: request - .collect_billing_details_from_wallet_connector - .or(Some(false)), - }) - } + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "authentication_connector_details", + })?, + payout_link_config: request + .payout_link_config + .as_ref() + .map(Encode::encode_to_value) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "payout_link_config", + })?, + is_connector_agnostic_mit_enabled: request.is_connector_agnostic_mit_enabled, + is_extended_card_info_enabled: None, + extended_card_info_config: None, + use_billing_as_payment_method_billing: request + .use_billing_as_payment_method_billing + .or(Some(true)), + collect_shipping_details_from_wallet_connector: request + .collect_shipping_details_from_wallet_connector + .or(Some(false)), + 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), + }) } diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 05bd978e12..a8d5c28b64 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -164,8 +164,10 @@ impl ProcessTrackerWorkflow 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, diff --git a/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/down.sql b/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/down.sql new file mode 100644 index 0000000000..005fc2b9e2 --- /dev/null +++ b/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/down.sql @@ -0,0 +1 @@ +ALTER TABLE business_profile DROP COLUMN IF EXISTS outgoing_webhook_custom_http_headers; \ No newline at end of file diff --git a/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/up.sql b/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/up.sql new file mode 100644 index 0000000000..b6bd1fce4d --- /dev/null +++ b/migrations/2024-07-10-065816_add_custom_outgoing_webhook_http_headers_to_business_profile/up.sql @@ -0,0 +1 @@ +ALTER TABLE business_profile ADD COLUMN IF NOT EXISTS outgoing_webhook_custom_http_headers BYTEA DEFAULT NULL; \ No newline at end of file