diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index fe32965377..2e38fe7fbd 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -178,6 +178,11 @@ pub struct MerchantAccountUpdate { ///Will be used to expire client secret after certain amount of time to be supplied in seconds ///(900) for 15 mins pub intent_fulfillment_time: Option, + + /// The default business profile that must be used for creating merchant accounts and payments + /// To unset this field, pass an empty string + #[schema(max_length = 64)] + pub default_profile: Option, } #[derive(Clone, Debug, ToSchema, Serialize)] @@ -263,6 +268,10 @@ pub struct MerchantAccountResponse { /// A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false pub is_recon_enabled: bool, + + /// The default business profile that must be used for creating merchant accounts and payments + #[schema(max_length = 64)] + pub default_profile: Option, } #[derive(Clone, Debug, Deserialize, ToSchema, Serialize)] @@ -617,6 +626,9 @@ pub struct MerchantConnectorCreate { } }))] pub connector_webhook_details: Option, + + /// Identifier for the business profile, if not provided default will be chosen from merchant account + pub profile_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] @@ -711,6 +723,11 @@ pub struct MerchantConnectorResponse { } }))] pub connector_webhook_details: Option, + + /// The business profile this connector must be created in + /// default value from merchant account is taken if not passed + #[schema(max_length = 64)] + pub profile_id: Option, } /// Create a new Merchant Connector for the merchant account. The connector could be a payment processor / facilitator / acquirer or specialized services like Fraud / Accounting etc." @@ -936,7 +953,7 @@ pub enum PayoutStraightThroughAlgorithm { Single(api_enums::PayoutConnectors), } -#[derive(Clone, Debug, Deserialize, ToSchema)] +#[derive(Clone, Debug, Deserialize, ToSchema, Default)] #[serde(deny_unknown_fields)] pub struct BusinessProfileCreate { /// A short name to identify the business profile diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index cbb2055260..bcc274c156 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -294,6 +294,10 @@ pub struct PaymentsRequest { /// additional data that might be required by hyperswitch pub feature_metadata: Option, + + /// The business profile to use for this payment, if not passed the default business profile + /// associated with the merchant account will be used. + pub profile_id: Option, } #[derive( @@ -1898,6 +1902,9 @@ pub struct PaymentsResponse { /// reference to the payment at connector side #[schema(value_type = Option, example = "993672945374576J")] pub reference_id: Option, + + /// The business profile that is associated with this payment + pub profile_id: Option, } #[derive(Clone, Debug, serde::Deserialize, ToSchema)] diff --git a/crates/data_models/src/payments/payment_intent.rs b/crates/data_models/src/payments/payment_intent.rs index cb31c8139e..824049837e 100644 --- a/crates/data_models/src/payments/payment_intent.rs +++ b/crates/data_models/src/payments/payment_intent.rs @@ -90,6 +90,7 @@ pub struct PaymentIntent { pub connector_metadata: Option, pub feature_metadata: Option, pub attempt_count: i16, + pub profile_id: Option, } #[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] @@ -126,6 +127,7 @@ pub struct PaymentIntentNew { pub connector_metadata: Option, pub feature_metadata: Option, pub attempt_count: i16, + pub profile_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/diesel_models/src/dispute.rs b/crates/diesel_models/src/dispute.rs index c481665308..1e1609491f 100644 --- a/crates/diesel_models/src/dispute.rs +++ b/crates/diesel_models/src/dispute.rs @@ -27,6 +27,7 @@ pub struct DisputeNew { pub connector_updated_at: Option, pub connector: String, pub evidence: Option>, + pub profile_id: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Identifiable, Queryable)] @@ -55,6 +56,7 @@ pub struct Dispute { pub modified_at: PrimitiveDateTime, pub connector: String, pub evidence: Secret, + pub profile_id: Option, } #[derive(Debug)] diff --git a/crates/diesel_models/src/merchant_account.rs b/crates/diesel_models/src/merchant_account.rs index 8b0833f88e..3f3c12e9aa 100644 --- a/crates/diesel_models/src/merchant_account.rs +++ b/crates/diesel_models/src/merchant_account.rs @@ -38,6 +38,7 @@ pub struct MerchantAccount { pub payout_routing_algorithm: Option, pub organization_id: Option, pub is_recon_enabled: bool, + pub default_profile: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -65,6 +66,7 @@ pub struct MerchantAccountNew { pub payout_routing_algorithm: Option, pub organization_id: Option, pub is_recon_enabled: bool, + pub default_profile: Option, } #[derive(Clone, Debug, Default, AsChangeset, router_derive::DebugAsDisplay)] @@ -91,4 +93,5 @@ pub struct MerchantAccountUpdateInternal { pub payout_routing_algorithm: Option, pub organization_id: Option, pub is_recon_enabled: bool, + pub default_profile: Option>, } diff --git a/crates/diesel_models/src/merchant_connector_account.rs b/crates/diesel_models/src/merchant_connector_account.rs index 91ecb87ea8..29d4709050 100644 --- a/crates/diesel_models/src/merchant_connector_account.rs +++ b/crates/diesel_models/src/merchant_connector_account.rs @@ -38,6 +38,7 @@ pub struct MerchantConnectorAccount { pub connector_webhook_details: Option, #[diesel(deserialize_as = super::OptionalDieselArray)] pub frm_config: Option>>, + pub profile_id: Option, } #[derive(Clone, Debug, Insertable, router_derive::DebugAsDisplay)] @@ -62,6 +63,7 @@ pub struct MerchantConnectorAccountNew { pub connector_webhook_details: Option, #[diesel(deserialize_as = super::OptionalDieselArray)] pub frm_config: Option>>, + pub profile_id: Option, } #[derive(Clone, Debug, AsChangeset, router_derive::DebugAsDisplay)] diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index d9abb0f23e..4b34523336 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -42,6 +42,7 @@ pub struct PaymentIntent { pub connector_metadata: Option, pub feature_metadata: Option, pub attempt_count: i16, + pub profile_id: Option, } #[derive( @@ -90,6 +91,7 @@ pub struct PaymentIntentNew { pub connector_metadata: Option, pub feature_metadata: Option, pub attempt_count: i16, + pub profile_id: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/diesel_models/src/payout_attempt.rs b/crates/diesel_models/src/payout_attempt.rs index 86c7e65310..64ba105511 100644 --- a/crates/diesel_models/src/payout_attempt.rs +++ b/crates/diesel_models/src/payout_attempt.rs @@ -26,6 +26,7 @@ pub struct PayoutAttempt { pub created_at: PrimitiveDateTime, #[serde(with = "common_utils::custom_serde::iso8601")] pub last_modified_at: PrimitiveDateTime, + pub profile_id: Option, } impl Default for PayoutAttempt { @@ -49,6 +50,7 @@ impl Default for PayoutAttempt { business_label: None, created_at: now, last_modified_at: now, + profile_id: None, } } } @@ -85,6 +87,7 @@ pub struct PayoutAttemptNew { pub created_at: Option, #[serde(default, with = "common_utils::custom_serde::iso8601::option")] pub last_modified_at: Option, + pub profile_id: Option, } #[derive(Debug)] diff --git a/crates/diesel_models/src/refund.rs b/crates/diesel_models/src/refund.rs index 595d54ea7f..eba0ff84cc 100644 --- a/crates/diesel_models/src/refund.rs +++ b/crates/diesel_models/src/refund.rs @@ -36,6 +36,7 @@ pub struct Refund { pub attempt_id: String, pub refund_reason: Option, pub refund_error_code: Option, + pub profile_id: Option, } #[derive( @@ -75,6 +76,7 @@ pub struct RefundNew { pub description: Option, pub attempt_id: String, pub refund_reason: Option, + pub profile_id: Option, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index e580da33a2..87cec69988 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -235,6 +235,8 @@ diesel::table! { #[max_length = 255] connector -> Varchar, evidence -> Jsonb, + #[max_length = 64] + profile_id -> Nullable, } } @@ -428,6 +430,8 @@ diesel::table! { #[max_length = 32] organization_id -> Nullable, is_recon_enabled -> Bool, + #[max_length = 64] + default_profile -> Nullable, } } @@ -461,6 +465,8 @@ diesel::table! { modified_at -> Timestamp, connector_webhook_details -> Nullable, frm_config -> Nullable>>, + #[max_length = 64] + profile_id -> Nullable, } } @@ -586,6 +592,8 @@ diesel::table! { connector_metadata -> Nullable, feature_metadata -> Nullable, attempt_count -> Int2, + #[max_length = 64] + profile_id -> Nullable, } } @@ -661,6 +669,8 @@ diesel::table! { business_label -> Nullable, created_at -> Timestamp, last_modified_at -> Timestamp, + #[max_length = 64] + profile_id -> Nullable, } } @@ -764,6 +774,8 @@ diesel::table! { #[max_length = 255] refund_reason -> Nullable, refund_error_code -> Nullable, + #[max_length = 64] + profile_id -> Nullable, } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index d318a29d24..406a4eb755 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -2,7 +2,7 @@ use api_models::{admin as admin_types, enums as api_enums}; use common_utils::{ crypto::{generate_cryptographically_secure_random_string, OptionalSecretValue}, date_time, - ext_traits::{Encode, ValueExt}, + ext_traits::{ConfigExt, Encode, ValueExt}, }; use data_models::MerchantStorageScheme; use error_stack::{report, FutureExt, ResultExt}; @@ -14,6 +14,7 @@ use crate::{ core::{ errors::{self, RouterResponse, RouterResult, StorageErrorExt}, payments::helpers, + utils as core_utils, }, db::StorageInterface, routes::metrics, @@ -118,6 +119,7 @@ pub async fn create_merchant_account( &key_store, ) .await?; + let metadata = req .metadata .as_ref() @@ -129,7 +131,8 @@ pub async fn create_merchant_account( }) .transpose()? .map(Secret::new); - let merchant_account = async { + + let mut merchant_account = async { Ok(domain::MerchantAccount { merchant_id: req.merchant_id, merchant_name: req @@ -162,11 +165,23 @@ pub async fn create_merchant_account( id: None, organization_id: req.organization_id, is_recon_enabled: false, + default_profile: None, }) } .await .change_context(errors::ApiErrorResponse::InternalServerError)?; + // Create a default business profile + let business_profile = create_and_insert_business_profile( + db, + api_models::admin::BusinessProfileCreate::default(), + merchant_account.clone(), + ) + .await?; + + // Update merchant account with the business profile id + merchant_account.default_profile = Some(business_profile.profile_id); + let merchant_account = db .insert_merchant(merchant_account, &key_store) .await @@ -253,6 +268,20 @@ pub async fn merchant_account_update( let key = key_store.key.get_inner().peek(); + let business_profile_id_update = if let Some(profile_id) = req.default_profile { + if !profile_id.is_empty_after_trim() { + // Validate whether profile_id passed in request is valid and is linked to the merchant + core_utils::validate_and_get_business_profile(db, Some(&profile_id), merchant_id) + .await? + .map(|business_profile| Some(business_profile.profile_id)) + } else { + // If empty, Update profile_id to None in the database + Some(None) + } + } else { + None + }; + let updated_merchant_account = storage::MerchantAccountUpdate::Update { merchant_name: req .merchant_name @@ -304,6 +333,7 @@ pub async fn merchant_account_update( frm_routing_algorithm: req.frm_routing_algorithm, intent_fulfillment_time: req.intent_fulfillment_time.map(i64::from), payout_routing_algorithm: req.payout_routing_algorithm, + default_profile: business_profile_id_update, }; let response = db @@ -480,6 +510,15 @@ pub async fn create_payment_connector( let frm_configs = get_frm_config_as_secret(req.frm_configs); + // Validate whether profile_id passed in request is valid and is linked to the merchant + let business_profile_from_request = + core_utils::validate_and_get_business_profile(store, req.profile_id.as_ref(), merchant_id) + .await?; + + let profile_id = business_profile_from_request + .map(|business_profile| business_profile.profile_id) + .or(merchant_account.default_profile.clone()); + let merchant_connector_account = domain::MerchantConnectorAccount { merchant_id: merchant_id.to_string(), connector_type: req.connector_type, @@ -520,6 +559,7 @@ pub async fn create_payment_connector( } None => None, }, + profile_id, }; let mca = store @@ -845,42 +885,45 @@ pub fn get_frm_config_as_secret( } } +pub async fn create_and_insert_business_profile( + db: &dyn StorageInterface, + request: api::BusinessProfileCreate, + merchant_account: domain::MerchantAccount, +) -> RouterResult { + let business_profile_new = storage::business_profile::BusinessProfileNew::foreign_try_from(( + merchant_account, + request, + ))?; + + db.insert_business_profile(business_profile_new) + .await + .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Failed to insert Business profile because of duplication error") +} + pub async fn create_business_profile( db: &dyn StorageInterface, request: api::BusinessProfileCreate, merchant_id: &str, + merchant_account: Option, ) -> RouterResponse { - 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)?; - - // Get the merchant account, if few fields are not passed, then they will be inherited from - // merchant account - let merchant_account = db - .find_merchant_account_by_merchant_id(merchant_id, &key_store) - .await - .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; - - // Generate a unique profile id - let profile_id = common_utils::generate_id_with_default_len("pro"); - - let payment_response_hash_key = request - .payment_response_hash_key - .or(merchant_account.payment_response_hash_key) - .unwrap_or(generate_cryptographically_secure_random_string(64)); - - let webhook_details = request - .webhook_details - .as_ref() - .map(|webhook_details| { - utils::Encode::::encode_to_value(webhook_details).change_context( - errors::ApiErrorResponse::InvalidDataValue { - field_name: "webhook details", - }, + let merchant_account = if let Some(merchant_account) = merchant_account { + merchant_account + } else { + let key_store = db + .get_merchant_key_store_by_merchant_id( + merchant_id, + &db.get_master_key().to_vec().into(), ) - }) - .transpose()?; + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + // Get the merchant account, if few fields are not passed, then they will be inherited from + // merchant account + db.find_merchant_account_by_merchant_id(merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)? + }; if let Some(ref routing_algorithm) = request.routing_algorithm { let _: api::RoutingAlgorithm = routing_algorithm @@ -892,46 +935,9 @@ pub async fn create_business_profile( .attach_printable("Invalid routing algorithm given")?; } - let business_profile_new = storage::business_profile::BusinessProfileNew { - profile_id, - merchant_id: merchant_id.to_string(), - profile_name: request.profile_name.unwrap_or("default".to_string()), - created_at: date_time::now(), - modified_at: date_time::now(), - 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: request - .routing_algorithm - .or(merchant_account.routing_algorithm), - intent_fulfillment_time: request - .intent_fulfillment_time - .map(i64::from) - .or(merchant_account.intent_fulfillment_time), - frm_routing_algorithm: request - .frm_routing_algorithm - .or(merchant_account.frm_routing_algorithm), - payout_routing_algorithm: request - .payout_routing_algorithm - .or(merchant_account.payout_routing_algorithm), - is_recon_enabled: merchant_account.is_recon_enabled, - }; + let business_profile = + create_and_insert_business_profile(db, request, merchant_account).await?; - let business_profile = db - .insert_business_profile(business_profile_new) - .await - .to_duplicate_response(errors::ApiErrorResponse::InternalServerError) - .attach_printable("Failed to insert Business profile because of duplication error")?; Ok(service_api::ApplicationResponse::Json( api_models::admin::BusinessProfileResponse::foreign_try_from(business_profile) .change_context(errors::ApiErrorResponse::InternalServerError)?, diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index d66e626e92..bc84010d5b 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -2204,6 +2204,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + profile_id: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(900); @@ -2248,6 +2249,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + profile_id: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); @@ -2292,6 +2294,7 @@ mod tests { connector_metadata: None, feature_metadata: None, attempt_count: 1, + profile_id: None, }; let req_cs = Some("1".to_string()); let merchant_fulfillment_time = Some(10); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index da0d08c07c..82a616773e 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -14,7 +14,7 @@ use crate::{ core::{ errors::{self, CustomResult, RouterResult, StorageErrorExt}, payments::{self, helpers, operations, CustomerDetails, PaymentAddress, PaymentData}, - utils as core_utils, + utils::{self as core_utils}, }, db::StorageInterface, routes::AppState, @@ -65,6 +65,18 @@ impl GetTracker, api::PaymentsRequest> for Pa .get_payment_intent_id() .change_context(errors::ApiErrorResponse::PaymentNotFound)?; + // Validate whether profile_id passed in request is valid and is linked to the merchant + let business_profile_from_request = core_utils::validate_and_get_business_profile( + db, + request.profile_id.as_ref(), + merchant_id, + ) + .await?; + + let profile_id = business_profile_from_request + .map(|business_profile| business_profile.profile_id) + .or(merchant_account.default_profile.clone()); + let ( token, payment_method, @@ -143,6 +155,7 @@ impl GetTracker, api::PaymentsRequest> for Pa shipping_address.clone().map(|x| x.address_id), billing_address.clone().map(|x| x.address_id), payment_attempt.attempt_id.to_owned(), + profile_id, )?, storage_scheme, ) @@ -569,6 +582,7 @@ impl PaymentCreate { } #[instrument(skip_all)] + #[allow(clippy::too_many_arguments)] fn make_payment_intent( payment_id: &str, merchant_account: &types::domain::MerchantAccount, @@ -577,6 +591,7 @@ impl PaymentCreate { shipping_address_id: Option, billing_address_id: Option, active_attempt_id: String, + profile_id: Option, ) -> RouterResult { let created_at @ modified_at @ last_synced = Some(common_utils::date_time::now()); let status = @@ -659,6 +674,7 @@ impl PaymentCreate { connector_metadata, feature_metadata, attempt_count: 1, + profile_id, }) } diff --git a/crates/router/src/core/payments/transformers.rs b/crates/router/src/core/payments/transformers.rs index 88907d816b..9daa2f38c5 100644 --- a/crates/router/src/core/payments/transformers.rs +++ b/crates/router/src/core/payments/transformers.rs @@ -576,6 +576,7 @@ where .set_feature_metadata(payment_intent.feature_metadata) .set_connector_metadata(payment_intent.connector_metadata) .set_reference_id(payment_attempt.connector_response_reference_id) + .set_profile_id(payment_intent.profile_id) .to_owned(), headers, )) diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index bb301ba086..fa4d80877a 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,9 +1,9 @@ use std::marker::PhantomData; use api_models::enums::{DisputeStage, DisputeStatus}; -use common_utils::errors::CustomResult; #[cfg(feature = "payouts")] use common_utils::{crypto::Encryptable, pii::Email}; +use common_utils::{errors::CustomResult, ext_traits::AsyncExt}; use error_stack::{IntoReport, ResultExt}; use router_env::{instrument, tracing}; use uuid::Uuid; @@ -16,7 +16,8 @@ use crate::core::payments; use crate::{ configs::settings, consts, - core::errors::{self, RouterResult}, + core::errors::{self, RouterResult, StorageErrorExt}, + db::StorageInterface, routes::AppState, types::{ self, domain, @@ -839,3 +840,31 @@ pub fn get_connector_request_reference_id( payment_attempt.attempt_id.clone() } } + +/// Validate whether the profile_id exists and is associated with the merchant_id +pub async fn validate_and_get_business_profile( + db: &dyn StorageInterface, + profile_id: Option<&String>, + merchant_id: &str, +) -> RouterResult> { + profile_id + .async_map(|profile_id| async { + db.find_business_profile_by_profile_id(profile_id) + .await + .to_not_found_response(errors::ApiErrorResponse::BusinessProfileNotFound { + id: profile_id.to_owned(), + }) + }) + .await + .transpose()? + .map(|business_profile| { + // Check if the merchant_id of business profile is same as the current merchant_id + if business_profile.merchant_id.ne(merchant_id) { + Err(errors::ApiErrorResponse::AccessForbidden) + } else { + Ok(business_profile) + } + }) + .transpose() + .into_report() +} diff --git a/crates/router/src/core/webhooks.rs b/crates/router/src/core/webhooks.rs index c605afd525..f9d4d59afe 100644 --- a/crates/router/src/core/webhooks.rs +++ b/crates/router/src/core/webhooks.rs @@ -306,6 +306,7 @@ pub async fn get_or_update_dispute_object( challenge_required_by: dispute_details.challenge_required_by, connector_created_at: dispute_details.created_at, connector_updated_at: dispute_details.updated_at, + profile_id: None, evidence: None, }; state diff --git a/crates/router/src/db/dispute.rs b/crates/router/src/db/dispute.rs index 43bbbadd1e..212e47a507 100644 --- a/crates/router/src/db/dispute.rs +++ b/crates/router/src/db/dispute.rs @@ -167,6 +167,7 @@ impl DisputeInterface for MockDb { created_at: now, modified_at: now, connector: dispute.connector, + profile_id: dispute.profile_id, evidence, }; @@ -399,6 +400,7 @@ mod tests { connector_updated_at: Some(datetime!(2019-01-03 0:00)), connector: "connector".into(), evidence: Some(Secret::from(Value::String("evidence".into()))), + profile_id: None, } } diff --git a/crates/router/src/db/merchant_connector_account.rs b/crates/router/src/db/merchant_connector_account.rs index 65da0069fe..2654d1ceaf 100644 --- a/crates/router/src/db/merchant_connector_account.rs +++ b/crates/router/src/db/merchant_connector_account.rs @@ -562,6 +562,7 @@ impl MerchantConnectorAccountInterface for MockDb { created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), connector_webhook_details: t.connector_webhook_details, + profile_id: t.profile_id, }; accounts.push(account.clone()); account @@ -751,6 +752,7 @@ mod merchant_connector_account_cache_tests { created_at: date_time::now(), modified_at: date_time::now(), connector_webhook_details: None, + profile_id: None, }; db.insert_merchant_connector_account(mca, &merchant_key) diff --git a/crates/router/src/db/payment_intent.rs b/crates/router/src/db/payment_intent.rs index 64ab05cf84..dc1c30b326 100644 --- a/crates/router/src/db/payment_intent.rs +++ b/crates/router/src/db/payment_intent.rs @@ -87,6 +87,7 @@ impl PaymentIntentInterface for MockDb { connector_metadata: new.connector_metadata, feature_metadata: new.feature_metadata, attempt_count: new.attempt_count, + profile_id: new.profile_id, }; payment_intents.push(payment_intent.clone()); Ok(payment_intent) diff --git a/crates/router/src/db/refund.rs b/crates/router/src/db/refund.rs index de18c1c0c2..4ef50d4552 100644 --- a/crates/router/src/db/refund.rs +++ b/crates/router/src/db/refund.rs @@ -357,6 +357,7 @@ mod storage { updated_at: new.created_at.unwrap_or_else(date_time::now), description: new.description.clone(), refund_reason: new.refund_reason.clone(), + profile_id: new.profile_id.clone(), }; let field = format!( @@ -759,6 +760,7 @@ impl RefundInterface for MockDb { updated_at: current_time, description: new.description, refund_reason: new.refund_reason.clone(), + profile_id: new.profile_id, }; refunds.push(refund.clone()); Ok(refund) diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index 225b9bad82..a0892abfcd 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -402,7 +402,7 @@ pub async fn business_profile_create( state.get_ref(), &req, payload, - |state, _, req| create_business_profile(&*state.store, req, &merchant_id), + |state, _, req| create_business_profile(&*state.store, req, &merchant_id, None), &auth::AdminApiAuth, ) .await diff --git a/crates/router/src/types/api/admin.rs b/crates/router/src/types/api/admin.rs index eb809155af..52d774464a 100644 --- a/crates/router/src/types/api/admin.rs +++ b/crates/router/src/types/api/admin.rs @@ -8,6 +8,7 @@ pub use api_models::admin::{ RoutingAlgorithm, StraightThroughAlgorithm, ToggleKVRequest, ToggleKVResponse, WebhookDetails, }; use common_utils::ext_traits::ValueExt; +use error_stack::ResultExt; use masking::Secret; use crate::{ @@ -43,6 +44,7 @@ impl TryFrom for MerchantAccountResponse { payout_routing_algorithm: item.payout_routing_algorithm, organization_id: item.organization_id, is_recon_enabled: item.is_recon_enabled, + default_profile: item.default_profile, }) } } @@ -70,3 +72,69 @@ impl ForeignTryFrom for BusinessProf }) } } + +impl ForeignTryFrom<(domain::MerchantAccount, BusinessProfileCreate)> + for storage::business_profile::BusinessProfileNew +{ + type Error = error_stack::Report; + + 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 webhook_details = request + .webhook_details + .as_ref() + .map(|webhook_details| { + common_utils::ext_traits::Encode::::encode_to_value(webhook_details) + .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)); + + 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: request + .routing_algorithm + .or(merchant_account.routing_algorithm), + intent_fulfillment_time: request + .intent_fulfillment_time + .map(i64::from) + .or(merchant_account.intent_fulfillment_time), + frm_routing_algorithm: request + .frm_routing_algorithm + .or(merchant_account.frm_routing_algorithm), + payout_routing_algorithm: request + .payout_routing_algorithm + .or(merchant_account.payout_routing_algorithm), + is_recon_enabled: merchant_account.is_recon_enabled, + }) + } +} diff --git a/crates/router/src/types/domain/merchant_account.rs b/crates/router/src/types/domain/merchant_account.rs index 1f089f4271..f75955c332 100644 --- a/crates/router/src/types/domain/merchant_account.rs +++ b/crates/router/src/types/domain/merchant_account.rs @@ -42,6 +42,7 @@ pub struct MerchantAccount { pub payout_routing_algorithm: Option, pub organization_id: Option, pub is_recon_enabled: bool, + pub default_profile: Option, } #[allow(clippy::large_enum_variant)] @@ -65,6 +66,7 @@ pub enum MerchantAccountUpdate { intent_fulfillment_time: Option, frm_routing_algorithm: Option, payout_routing_algorithm: Option, + default_profile: Option>, }, StorageSchemeUpdate { storage_scheme: MerchantStorageScheme, @@ -95,6 +97,7 @@ impl From for MerchantAccountUpdateInternal { intent_fulfillment_time, frm_routing_algorithm, payout_routing_algorithm, + default_profile, } => Self { merchant_name: merchant_name.map(Encryption::from), merchant_details: merchant_details.map(Encryption::from), @@ -114,6 +117,7 @@ impl From for MerchantAccountUpdateInternal { modified_at: Some(date_time::now()), intent_fulfillment_time, payout_routing_algorithm, + default_profile, ..Default::default() }, MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { @@ -161,6 +165,7 @@ impl super::behaviour::Conversion for MerchantAccount { payout_routing_algorithm: self.payout_routing_algorithm, organization_id: self.organization_id, is_recon_enabled: self.is_recon_enabled, + default_profile: self.default_profile, }) } @@ -203,6 +208,7 @@ impl super::behaviour::Conversion for MerchantAccount { payout_routing_algorithm: item.payout_routing_algorithm, organization_id: item.organization_id, is_recon_enabled: item.is_recon_enabled, + default_profile: item.default_profile, }) } .await @@ -236,6 +242,7 @@ impl super::behaviour::Conversion for MerchantAccount { payout_routing_algorithm: self.payout_routing_algorithm, organization_id: self.organization_id, is_recon_enabled: self.is_recon_enabled, + default_profile: self.default_profile, }) } } diff --git a/crates/router/src/types/domain/merchant_connector_account.rs b/crates/router/src/types/domain/merchant_connector_account.rs index 95b1bd1de4..a013534926 100644 --- a/crates/router/src/types/domain/merchant_connector_account.rs +++ b/crates/router/src/types/domain/merchant_connector_account.rs @@ -32,6 +32,7 @@ pub struct MerchantConnectorAccount { pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub connector_webhook_details: Option, + pub profile_id: Option, } #[derive(Debug)] @@ -80,6 +81,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { created_at: self.created_at, modified_at: self.modified_at, connector_webhook_details: self.connector_webhook_details, + profile_id: self.profile_id, }, ) } @@ -116,6 +118,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { created_at: other.created_at, modified_at: other.modified_at, connector_webhook_details: other.connector_webhook_details, + profile_id: other.profile_id, }) } @@ -140,6 +143,7 @@ impl behaviour::Conversion for MerchantConnectorAccount { created_at: now, modified_at: now, connector_webhook_details: self.connector_webhook_details, + profile_id: self.profile_id, }) } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 54b15431eb..bfe4450fa6 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -607,6 +607,7 @@ impl TryFrom for api_models::admin::MerchantCo .change_context(errors::ApiErrorResponse::InternalServerError) }) .transpose()?, + profile_id: item.profile_id, }) } } diff --git a/crates/storage_impl/src/payments/payment_intent.rs b/crates/storage_impl/src/payments/payment_intent.rs index 3150707271..72437afd29 100644 --- a/crates/storage_impl/src/payments/payment_intent.rs +++ b/crates/storage_impl/src/payments/payment_intent.rs @@ -89,6 +89,7 @@ impl PaymentIntentInterface for KVRouterStore { connector_metadata: new.connector_metadata.clone(), feature_metadata: new.feature_metadata.clone(), attempt_count: new.attempt_count, + profile_id: new.profile_id.clone(), }; match self @@ -603,6 +604,7 @@ impl DataModelExt for PaymentIntentNew { connector_metadata: self.connector_metadata, feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, + profile_id: self.profile_id, } } @@ -637,6 +639,7 @@ impl DataModelExt for PaymentIntentNew { connector_metadata: storage_model.connector_metadata, feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, + profile_id: storage_model.profile_id, } } } @@ -676,6 +679,7 @@ impl DataModelExt for PaymentIntent { connector_metadata: self.connector_metadata, feature_metadata: self.feature_metadata, attempt_count: self.attempt_count, + profile_id: self.profile_id, } } @@ -711,6 +715,7 @@ impl DataModelExt for PaymentIntent { connector_metadata: storage_model.connector_metadata, feature_metadata: storage_model.feature_metadata, attempt_count: storage_model.attempt_count, + profile_id: storage_model.profile_id, } } } diff --git a/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/down.sql b/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/down.sql new file mode 100644 index 0000000000..9ffb6c9467 --- /dev/null +++ b/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/down.sql @@ -0,0 +1,11 @@ +ALTER TABLE payment_intent DROP COLUMN IF EXISTS profile_id; + +ALTER TABLE merchant_connector_account DROP COLUMN IF EXISTS profile_id; + +ALTER TABLE merchant_account DROP COLUMN IF EXISTS default_profile; + +ALTER TABLE refund DROP COLUMN IF EXISTS profile_id; + +ALTER TABLE dispute DROP COLUMN IF EXISTS profile_id; + +ALTER TABLE payout_attempt DROP COLUMN IF EXISTS profile_id; diff --git a/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/up.sql b/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/up.sql new file mode 100644 index 0000000000..28c4359e34 --- /dev/null +++ b/migrations/2023-08-16-112847_add_profile_id_in_affected_tables/up.sql @@ -0,0 +1,21 @@ +-- Your SQL goes here +ALTER TABLE payment_intent +ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); + +ALTER TABLE merchant_connector_account +ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); + +ALTER TABLE merchant_account +ADD COLUMN IF NOT EXISTS default_profile VARCHAR(64); + +-- Profile id is needed in refunds for listing refunds by business profile +ALTER TABLE refund +ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); + +-- For listing disputes by business profile +ALTER TABLE dispute +ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); + +-- For a similar use case as to payments +ALTER TABLE payout_attempt +ADD COLUMN IF NOT EXISTS profile_id VARCHAR(64); diff --git a/openapi/openapi_spec.json b/openapi/openapi_spec.json index e94c91ea03..7a7bc9d891 100644 --- a/openapi/openapi_spec.json +++ b/openapi/openapi_spec.json @@ -6226,6 +6226,12 @@ "is_recon_enabled": { "type": "boolean", "description": "A boolean value to indicate if the merchant has recon service is enabled or not, by default value is false" + }, + "default_profile": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments", + "nullable": true, + "maxLength": 64 } } }, @@ -6352,6 +6358,12 @@ "description": "Will be used to expire client secret after certain amount of time to be supplied in seconds\n(900) for 15 mins", "nullable": true, "minimum": 0.0 + }, + "default_profile": { + "type": "string", + "description": "The default business profile that must be used for creating merchant accounts and payments\nTo unset this field, pass an empty string", + "nullable": true, + "maxLength": 64 } } }, @@ -6478,6 +6490,11 @@ } ], "nullable": true + }, + "profile_id": { + "type": "string", + "description": "Identifier for the business profile, if not provided default will be chosen from merchant account", + "nullable": true } } }, @@ -6684,6 +6701,12 @@ } ], "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The business profile this connector must be created in\ndefault value from merchant account is taken if not passed", + "nullable": true, + "maxLength": 64 } } }, @@ -8653,6 +8676,11 @@ } ], "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "nullable": true } } }, @@ -8987,6 +9015,11 @@ } ], "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The business profile to use for this payment, if not passed the default business profile\nassociated with the merchant account will be used.", + "nullable": true } } }, @@ -9363,6 +9396,11 @@ "description": "reference to the payment at connector side", "example": "993672945374576J", "nullable": true + }, + "profile_id": { + "type": "string", + "description": "The business profile that is associated with this payment", + "nullable": true } } },