diff --git a/.github/workflows/postman-collection-runner.yml b/.github/workflows/postman-collection-runner.yml index de8235c14ea4..b8ce65f4b6c8 100644 --- a/.github/workflows/postman-collection-runner.yml +++ b/.github/workflows/postman-collection-runner.yml @@ -17,7 +17,7 @@ env: CONNECTORS: stripe RUST_BACKTRACE: short RUSTUP_MAX_RETRIES: 10 - RUST_MIN_STACK: 8388608 + RUST_MIN_STACK: 10485760 jobs: runner: diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 7fdc0c6d5e83..65ce1fdbc0a6 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -428,3 +428,6 @@ card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] connector_list = "cybersource" + +[platform] +enabled = true diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 9c91c12f3b43..8900b0ac3856 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -444,3 +444,6 @@ card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] connector_list = "cybersource" + +[platform] +enabled = false diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 152f4f04cd89..ae4afb2ec3ff 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -446,3 +446,6 @@ card_networks = "Visa, AmericanExpress, Mastercard" [network_tokenization_supported_connectors] connector_list = "cybersource" + +[platform] +enabled = false diff --git a/config/development.toml b/config/development.toml index 133823d6cb37..7e3c3c8ba070 100644 --- a/config/development.toml +++ b/config/development.toml @@ -830,3 +830,6 @@ entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be foreground_color = "#000000" # Foreground color of email text primary_color = "#006DF9" # Primary color of email body background_color = "#FFFFFF" # Background color of email body + +[platform] +enabled = true diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 1ad2ef91336c..09b27d6bd7f4 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -710,3 +710,6 @@ entity_logo_url = "https://example.com/logo.svg" # Logo URL of the entity to be foreground_color = "#000000" # Foreground color of email text primary_color = "#006DF9" # Primary color of email body background_color = "#FFFFFF" # Background color of email body + +[platform] +enabled = true diff --git a/crates/diesel_models/src/merchant_account.rs b/crates/diesel_models/src/merchant_account.rs index 12d51311e874..b5c2bc285724 100644 --- a/crates/diesel_models/src/merchant_account.rs +++ b/crates/diesel_models/src/merchant_account.rs @@ -51,6 +51,7 @@ pub struct MerchantAccount { pub payment_link_config: Option, pub pm_collect_link_config: Option, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v1")] @@ -83,6 +84,7 @@ pub struct MerchantAccountSetter { pub payment_link_config: Option, pub pm_collect_link_config: Option, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v1")] @@ -117,6 +119,7 @@ impl From for MerchantAccount { payment_link_config: item.payment_link_config, pm_collect_link_config: item.pm_collect_link_config, version: item.version, + is_platform_account: item.is_platform_account, } } } @@ -148,6 +151,7 @@ pub struct MerchantAccount { pub recon_status: storage_enums::ReconStatus, pub version: common_enums::ApiVersion, pub id: common_utils::id_type::MerchantId, + pub is_platform_account: bool, } #[cfg(feature = "v2")] @@ -165,6 +169,7 @@ impl From for MerchantAccount { organization_id: item.organization_id, recon_status: item.recon_status, version: item.version, + is_platform_account: item.is_platform_account, } } } @@ -182,6 +187,7 @@ pub struct MerchantAccountSetter { pub organization_id: common_utils::id_type::OrganizationId, pub recon_status: storage_enums::ReconStatus, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } impl MerchantAccount { @@ -228,6 +234,7 @@ pub struct MerchantAccountNew { pub payment_link_config: Option, pub pm_collect_link_config: Option, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v2")] @@ -244,6 +251,7 @@ pub struct MerchantAccountNew { pub recon_status: storage_enums::ReconStatus, pub id: common_utils::id_type::MerchantId, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v2")] @@ -258,6 +266,7 @@ pub struct MerchantAccountUpdateInternal { pub modified_at: time::PrimitiveDateTime, pub organization_id: Option, pub recon_status: Option, + pub is_platform_account: Option, } #[cfg(feature = "v2")] @@ -272,6 +281,7 @@ impl MerchantAccountUpdateInternal { modified_at, organization_id, recon_status, + is_platform_account, } = self; MerchantAccount { @@ -286,6 +296,7 @@ impl MerchantAccountUpdateInternal { recon_status: recon_status.unwrap_or(source.recon_status), version: source.version, id: source.id, + is_platform_account: is_platform_account.unwrap_or(source.is_platform_account), } } } @@ -319,6 +330,7 @@ pub struct MerchantAccountUpdateInternal { pub recon_status: Option, pub payment_link_config: Option, pub pm_collect_link_config: Option, + pub is_platform_account: Option, } #[cfg(feature = "v1")] @@ -350,6 +362,7 @@ impl MerchantAccountUpdateInternal { recon_status, payment_link_config, pm_collect_link_config, + is_platform_account, } = self; MerchantAccount { @@ -385,6 +398,7 @@ impl MerchantAccountUpdateInternal { payment_link_config: payment_link_config.or(source.payment_link_config), pm_collect_link_config: pm_collect_link_config.or(source.pm_collect_link_config), version: source.version, + is_platform_account: is_platform_account.unwrap_or(source.is_platform_account), } } } diff --git a/crates/diesel_models/src/payment_intent.rs b/crates/diesel_models/src/payment_intent.rs index 8b834ee5d827..20c14d0dd104 100644 --- a/crates/diesel_models/src/payment_intent.rs +++ b/crates/diesel_models/src/payment_intent.rs @@ -74,6 +74,7 @@ pub struct PaymentIntent { pub id: common_utils::id_type::GlobalPaymentId, pub psd2_sca_exemption_type: Option, pub split_payments: Option, + pub platform_merchant_id: Option, } #[cfg(feature = "v1")] @@ -140,6 +141,7 @@ pub struct PaymentIntent { pub skip_external_tax_calculation: Option, pub psd2_sca_exemption_type: Option, pub split_payments: Option, + pub platform_merchant_id: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, diesel::AsExpression, PartialEq)] @@ -300,6 +302,7 @@ pub struct PaymentIntentNew { pub enable_payment_link: Option, pub apply_mit_exemption: Option, pub id: common_utils::id_type::GlobalPaymentId, + pub platform_merchant_id: Option, } #[cfg(feature = "v1")] @@ -366,6 +369,7 @@ pub struct PaymentIntentNew { pub tax_details: Option, pub skip_external_tax_calculation: Option, pub psd2_sca_exemption_type: Option, + pub platform_merchant_id: Option, pub split_payments: Option, } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index 366c917d2d11..95bb714cb711 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -717,6 +717,7 @@ diesel::table! { payment_link_config -> Nullable, pm_collect_link_config -> Nullable, version -> ApiVersion, + is_platform_account -> Bool, } } @@ -970,6 +971,8 @@ diesel::table! { skip_external_tax_calculation -> Nullable, psd2_sca_exemption_type -> Nullable, split_payments -> Nullable, + #[max_length = 64] + platform_merchant_id -> Nullable, } } diff --git a/crates/diesel_models/src/schema_v2.rs b/crates/diesel_models/src/schema_v2.rs index fcfe05e5731c..9faf3830605b 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -708,6 +708,7 @@ diesel::table! { version -> ApiVersion, #[max_length = 64] id -> Varchar, + is_platform_account -> Bool, } } @@ -933,6 +934,8 @@ diesel::table! { id -> Varchar, psd2_sca_exemption_type -> Nullable, split_payments -> Nullable, + #[max_length = 64] + platform_merchant_id -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs index 850cc4703167..6fa9f9450c85 100644 --- a/crates/hyperswitch_domain_models/src/errors/api_error_response.rs +++ b/crates/hyperswitch_domain_models/src/errors/api_error_response.rs @@ -279,7 +279,10 @@ pub enum ApiErrorResponse { message = "Cookies are not found in the request" )] CookieNotFound, - + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_43", message = "API does not support platform account operation")] + PlatformAccountAuthNotSupported, + #[error(error_type = ErrorType::InvalidRequestError, code = "IR_44", message = "Invalid platform account operation")] + InvalidPlatformOperation, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_01", message = "Failed to authenticate the webhook")] WebhookAuthenticationFailed, #[error(error_type = ErrorType::InvalidRequestError, code = "WE_02", message = "Bad request received in webhook")] @@ -667,6 +670,12 @@ impl ErrorSwitch for ApiErrorRespon ..Default::default() }) )), + Self::PlatformAccountAuthNotSupported => { + AER::BadRequest(ApiError::new("IR", 43, "API does not support platform operation", None)) + } + Self::InvalidPlatformOperation => { + AER::Unauthorized(ApiError::new("IR", 44, "Invalid platform account operation", None)) + } } } } diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs index a6d2114f0fe9..c42b0005513f 100644 --- a/crates/hyperswitch_domain_models/src/merchant_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -47,6 +47,7 @@ pub struct MerchantAccount { pub payment_link_config: Option, pub pm_collect_link_config: Option, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v1")] @@ -81,6 +82,7 @@ pub struct MerchantAccountSetter { pub payment_link_config: Option, pub pm_collect_link_config: Option, pub version: common_enums::ApiVersion, + pub is_platform_account: bool, } #[cfg(feature = "v1")] @@ -115,6 +117,7 @@ impl From for MerchantAccount { payment_link_config: item.payment_link_config, pm_collect_link_config: item.pm_collect_link_config, version: item.version, + is_platform_account: item.is_platform_account, } } } @@ -133,6 +136,7 @@ pub struct MerchantAccountSetter { pub modified_at: time::PrimitiveDateTime, pub organization_id: common_utils::id_type::OrganizationId, pub recon_status: diesel_models::enums::ReconStatus, + pub is_platform_account: bool, } #[cfg(feature = "v2")] @@ -149,6 +153,7 @@ impl From for MerchantAccount { modified_at, organization_id, recon_status, + is_platform_account, } = item; Self { id, @@ -161,6 +166,7 @@ impl From for MerchantAccount { modified_at, organization_id, recon_status, + is_platform_account, } } } @@ -178,6 +184,7 @@ pub struct MerchantAccount { pub modified_at: time::PrimitiveDateTime, pub organization_id: common_utils::id_type::OrganizationId, pub recon_status: diesel_models::enums::ReconStatus, + pub is_platform_account: bool, } impl MerchantAccount { @@ -233,6 +240,7 @@ pub enum MerchantAccountUpdate { }, UnsetDefaultProfile, ModifiedAtUpdate, + ToPlatformAccount, } #[cfg(feature = "v2")] @@ -252,6 +260,7 @@ pub enum MerchantAccountUpdate { recon_status: diesel_models::enums::ReconStatus, }, ModifiedAtUpdate, + ToPlatformAccount, } #[cfg(feature = "v1")] @@ -307,6 +316,7 @@ impl From for MerchantAccountUpdateInternal { organization_id: None, is_recon_enabled: None, recon_status: None, + is_platform_account: None, }, MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { storage_scheme: Some(storage_scheme), @@ -334,6 +344,7 @@ impl From for MerchantAccountUpdateInternal { recon_status: None, payment_link_config: None, pm_collect_link_config: None, + is_platform_account: None, }, MerchantAccountUpdate::ReconUpdate { recon_status } => Self { recon_status: Some(recon_status), @@ -361,6 +372,7 @@ impl From for MerchantAccountUpdateInternal { default_profile: None, payment_link_config: None, pm_collect_link_config: None, + is_platform_account: None, }, MerchantAccountUpdate::UnsetDefaultProfile => Self { default_profile: Some(None), @@ -388,6 +400,7 @@ impl From for MerchantAccountUpdateInternal { recon_status: None, payment_link_config: None, pm_collect_link_config: None, + is_platform_account: None, }, MerchantAccountUpdate::ModifiedAtUpdate => Self { modified_at: now, @@ -415,6 +428,35 @@ impl From for MerchantAccountUpdateInternal { recon_status: None, payment_link_config: None, pm_collect_link_config: None, + is_platform_account: None, + }, + MerchantAccountUpdate::ToPlatformAccount => Self { + modified_at: now, + merchant_name: None, + merchant_details: None, + return_url: None, + webhook_details: None, + sub_merchants_enabled: None, + parent_merchant_id: None, + enable_payment_response_hash: None, + payment_response_hash_key: None, + redirect_to_merchant_with_http_post: None, + publishable_key: None, + storage_scheme: None, + locker_id: None, + metadata: None, + routing_algorithm: None, + primary_business_details: None, + intent_fulfillment_time: None, + frm_routing_algorithm: None, + payout_routing_algorithm: None, + organization_id: None, + is_recon_enabled: None, + default_profile: None, + recon_status: None, + payment_link_config: None, + pm_collect_link_config: None, + is_platform_account: Some(true), }, } } @@ -440,6 +482,7 @@ impl From for MerchantAccountUpdateInternal { storage_scheme: None, organization_id: None, recon_status: None, + is_platform_account: None, }, MerchantAccountUpdate::StorageSchemeUpdate { storage_scheme } => Self { storage_scheme: Some(storage_scheme), @@ -450,6 +493,7 @@ impl From for MerchantAccountUpdateInternal { metadata: None, organization_id: None, recon_status: None, + is_platform_account: None, }, MerchantAccountUpdate::ReconUpdate { recon_status } => Self { recon_status: Some(recon_status), @@ -460,6 +504,7 @@ impl From for MerchantAccountUpdateInternal { storage_scheme: None, metadata: None, organization_id: None, + is_platform_account: None, }, MerchantAccountUpdate::ModifiedAtUpdate => Self { modified_at: now, @@ -470,6 +515,18 @@ impl From for MerchantAccountUpdateInternal { metadata: None, organization_id: None, recon_status: None, + is_platform_account: None, + }, + MerchantAccountUpdate::ToPlatformAccount => Self { + modified_at: now, + merchant_name: None, + merchant_details: None, + publishable_key: None, + storage_scheme: None, + metadata: None, + organization_id: None, + recon_status: None, + is_platform_account: Some(true), }, } } @@ -495,6 +552,7 @@ impl super::behaviour::Conversion for MerchantAccount { organization_id: self.organization_id, recon_status: self.recon_status, version: crate::consts::API_VERSION, + is_platform_account: self.is_platform_account, }; Ok(diesel_models::MerchantAccount::from(setter)) @@ -554,6 +612,7 @@ impl super::behaviour::Conversion for MerchantAccount { modified_at: item.modified_at, organization_id: item.organization_id, recon_status: item.recon_status, + is_platform_account: item.is_platform_account, }) } .await @@ -575,6 +634,7 @@ impl super::behaviour::Conversion for MerchantAccount { organization_id: self.organization_id, recon_status: self.recon_status, version: crate::consts::API_VERSION, + is_platform_account: self.is_platform_account, }) } } @@ -614,6 +674,7 @@ impl super::behaviour::Conversion for MerchantAccount { payment_link_config: self.payment_link_config, pm_collect_link_config: self.pm_collect_link_config, version: self.version, + is_platform_account: self.is_platform_account, }; Ok(diesel_models::MerchantAccount::from(setter)) @@ -691,6 +752,7 @@ impl super::behaviour::Conversion for MerchantAccount { payment_link_config: item.payment_link_config, pm_collect_link_config: item.pm_collect_link_config, version: item.version, + is_platform_account: item.is_platform_account, }) } .await @@ -729,6 +791,7 @@ impl super::behaviour::Conversion for MerchantAccount { payment_link_config: self.payment_link_config, pm_collect_link_config: self.pm_collect_link_config, version: crate::consts::API_VERSION, + is_platform_account: self.is_platform_account, }) } } diff --git a/crates/hyperswitch_domain_models/src/payments.rs b/crates/hyperswitch_domain_models/src/payments.rs index 1c88b83d087e..095b4c33a46e 100644 --- a/crates/hyperswitch_domain_models/src/payments.rs +++ b/crates/hyperswitch_domain_models/src/payments.rs @@ -109,6 +109,7 @@ pub struct PaymentIntent { pub tax_details: Option, pub skip_external_tax_calculation: Option, pub psd2_sca_exemption_type: Option, + pub platform_merchant_id: Option, } impl PaymentIntent { @@ -365,6 +366,8 @@ pub struct PaymentIntent { pub payment_link_config: Option, /// The straight through routing algorithm id that is used for this payment. This overrides the default routing algorithm that is configured in business profile. pub routing_algorithm_id: Option, + /// Identifier for the platform merchant. + pub platform_merchant_id: Option, } #[cfg(feature = "v2")] @@ -411,6 +414,7 @@ impl PaymentIntent { profile: &business_profile::Profile, request: api_models::payments::PaymentsCreateIntentRequest, decrypted_payment_intent: DecryptedPaymentIntent, + platform_merchant_id: Option<&merchant_account::MerchantAccount>, ) -> CustomResult { let connector_metadata = request .get_connector_metadata_as_value() @@ -504,6 +508,8 @@ impl PaymentIntent { .payment_link_config .map(ApiModelToDieselModelConvertor::convert_from), routing_algorithm_id: request.routing_algorithm_id, + platform_merchant_id: platform_merchant_id + .map(|merchant_account| merchant_account.get_id().to_owned()), }) } } diff --git a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs index cbd3a8137ca9..953d39c131a6 100644 --- a/crates/hyperswitch_domain_models/src/payments/payment_intent.rs +++ b/crates/hyperswitch_domain_models/src/payments/payment_intent.rs @@ -1326,6 +1326,7 @@ impl behaviour::Conversion for PaymentIntent { customer_present, routing_algorithm_id, payment_link_config, + platform_merchant_id, } = self; Ok(DieselPaymentIntent { skip_external_tax_calculation: Some(amount_details.get_external_tax_action_as_bool()), @@ -1396,6 +1397,7 @@ impl behaviour::Conversion for PaymentIntent { payment_link_config, routing_algorithm_id, psd2_sca_exemption_type: None, + platform_merchant_id, split_payments: None, }) } @@ -1522,6 +1524,7 @@ impl behaviour::Conversion for PaymentIntent { customer_present: storage_model.customer_present.into(), payment_link_config: storage_model.payment_link_config, routing_algorithm_id: storage_model.routing_algorithm_id, + platform_merchant_id: storage_model.platform_merchant_id, }) } .await @@ -1594,6 +1597,7 @@ impl behaviour::Conversion for PaymentIntent { tax_details: amount_details.tax_details, enable_payment_link: Some(self.enable_payment_link.as_bool()), apply_mit_exemption: Some(self.apply_mit_exemption.as_bool()), + platform_merchant_id: self.platform_merchant_id, }) } } @@ -1660,6 +1664,7 @@ impl behaviour::Conversion for PaymentIntent { tax_details: self.tax_details, skip_external_tax_calculation: self.skip_external_tax_calculation, psd2_sca_exemption_type: self.psd2_sca_exemption_type, + platform_merchant_id: self.platform_merchant_id, }) } @@ -1748,6 +1753,7 @@ impl behaviour::Conversion for PaymentIntent { organization_id: storage_model.organization_id, skip_external_tax_calculation: storage_model.skip_external_tax_calculation, psd2_sca_exemption_type: storage_model.psd2_sca_exemption_type, + platform_merchant_id: storage_model.platform_merchant_id, }) } .await @@ -1812,6 +1818,7 @@ impl behaviour::Conversion for PaymentIntent { tax_details: self.tax_details, skip_external_tax_calculation: self.skip_external_tax_calculation, psd2_sca_exemption_type: self.psd2_sca_exemption_type, + platform_merchant_id: self.platform_merchant_id, }) } } diff --git a/crates/router/build.rs b/crates/router/build.rs index b33c168833d2..7c8043c48ade 100644 --- a/crates/router/build.rs +++ b/crates/router/build.rs @@ -1,8 +1,8 @@ fn main() { - // Set thread stack size to 8 MiB for debug builds + // Set thread stack size to 10 MiB for debug builds // Reference: https://doc.rust-lang.org/std/thread/#stack-size #[cfg(debug_assertions)] - println!("cargo:rustc-env=RUST_MIN_STACK=8388608"); // 8 * 1024 * 1024 = 8 MiB + println!("cargo:rustc-env=RUST_MIN_STACK=10485760"); // 10 * 1024 * 1024 = 10 MiB #[cfg(feature = "vergen")] router_env::vergen::generate_cargo_instructions(); diff --git a/crates/router/src/compatibility/stripe/errors.rs b/crates/router/src/compatibility/stripe/errors.rs index 6cf078b5f817..630d4dfdca0e 100644 --- a/crates/router/src/compatibility/stripe/errors.rs +++ b/crates/router/src/compatibility/stripe/errors.rs @@ -278,6 +278,10 @@ pub enum StripeErrorCode { InvalidTenant, #[error(error_type = StripeErrorType::HyperswitchError, code = "HE_01", message = "Failed to convert amount to {amount_type} type")] AmountConversionFailed { amount_type: &'static str }, + #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Platform Bad Request")] + PlatformBadRequest, + #[error(error_type = StripeErrorType::HyperswitchError, code = "", message = "Platform Unauthorized Request")] + PlatformUnauthorizedRequest, // [#216]: https://github.com/juspay/hyperswitch/issues/216 // Implement the remaining stripe error codes @@ -678,6 +682,8 @@ impl From for StripeErrorCode { errors::ApiErrorResponse::AmountConversionFailed { amount_type } => { Self::AmountConversionFailed { amount_type } } + errors::ApiErrorResponse::PlatformAccountAuthNotSupported => Self::PlatformBadRequest, + errors::ApiErrorResponse::InvalidPlatformOperation => Self::PlatformUnauthorizedRequest, } } } @@ -687,7 +693,7 @@ impl actix_web::ResponseError for StripeErrorCode { use reqwest::StatusCode; match self { - Self::Unauthorized => StatusCode::UNAUTHORIZED, + Self::Unauthorized | Self::PlatformUnauthorizedRequest => StatusCode::UNAUTHORIZED, Self::InvalidRequestUrl | Self::GenericNotFoundError { .. } => StatusCode::NOT_FOUND, Self::ParameterUnknown { .. } | Self::HyperswitchUnprocessableEntity { .. } => { StatusCode::UNPROCESSABLE_ENTITY @@ -751,6 +757,7 @@ impl actix_web::ResponseError for StripeErrorCode { | Self::CurrencyConversionFailed | Self::PaymentMethodDeleteFailed | Self::ExtendedCardInfoNotFound + | Self::PlatformBadRequest | Self::LinkConfigurationError { .. } => StatusCode::BAD_REQUEST, Self::RefundFailed | Self::PayoutFailed diff --git a/crates/router/src/compatibility/stripe/payment_intents.rs b/crates/router/src/compatibility/stripe/payment_intents.rs index 5bbb4e7cf22a..6652f42ee00e 100644 --- a/crates/router/src/compatibility/stripe/payment_intents.rs +++ b/crates/router/src/compatibility/stripe/payment_intents.rs @@ -92,6 +92,7 @@ pub async fn payment_intents_create( payments::CallConnectorAction::Trigger, eligible_connectors, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -162,6 +163,7 @@ pub async fn payment_intents_retrieve( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -240,6 +242,7 @@ pub async fn payment_intents_retrieve_with_gateway_creds( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -316,6 +319,7 @@ pub async fn payment_intents_update( payments::CallConnectorAction::Trigger, eligible_connectors, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -401,6 +405,7 @@ pub async fn payment_intents_confirm( payments::CallConnectorAction::Trigger, eligible_connectors, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -472,6 +477,7 @@ pub async fn payment_intents_capture( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -547,6 +553,7 @@ pub async fn payment_intents_cancel( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, diff --git a/crates/router/src/compatibility/stripe/setup_intents.rs b/crates/router/src/compatibility/stripe/setup_intents.rs index 919ced993aad..6dde49b0d620 100644 --- a/crates/router/src/compatibility/stripe/setup_intents.rs +++ b/crates/router/src/compatibility/stripe/setup_intents.rs @@ -78,6 +78,7 @@ pub async fn setup_intents_create( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -148,6 +149,7 @@ pub async fn setup_intents_retrieve( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -224,6 +226,7 @@ pub async fn setup_intents_update( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -301,6 +304,7 @@ pub async fn setup_intents_confirm( payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 3862f70536fc..7b74aca76472 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -546,5 +546,6 @@ pub(crate) async fn fetch_raw_secrets( network_tokenization_service, network_tokenization_supported_connectors: conf.network_tokenization_supported_connectors, theme: conf.theme, + platform: conf.platform, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index ccd43d25b8ee..ad5d9e89aaae 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -129,6 +129,12 @@ pub struct Settings { pub network_tokenization_service: Option>, pub network_tokenization_supported_connectors: NetworkTokenizationSupportedConnectors, pub theme: ThemeSettings, + pub platform: Platform, +} + +#[derive(Debug, Deserialize, Clone, Default)] +pub struct Platform { + pub enabled: bool, } #[derive(Debug, Deserialize, Clone, Default)] diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index ccb563b06102..05aaf99421b6 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -402,6 +402,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { payment_link_config: None, pm_collect_link_config, version: hyperswitch_domain_models::consts::API_VERSION, + is_platform_account: false, }, ) } @@ -669,6 +670,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { modified_at: date_time::now(), organization_id: organization.get_organization_id(), recon_status: diesel_models::enums::ReconStatus::NotRequested, + is_platform_account: false, }), ) } @@ -4769,3 +4771,35 @@ async fn locker_recipient_create_call( Ok(store_resp.card_reference) } + +pub async fn enable_platform_account( + state: SessionState, + merchant_id: id_type::MerchantId, +) -> RouterResponse<()> { + let db = state.store.as_ref(); + let key_manager_state = &(&state).into(); + let key_store = db + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &merchant_id, + &db.get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let merchant_account = db + .find_merchant_account_by_merchant_id(key_manager_state, &merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + db.update_merchant( + key_manager_state, + merchant_account, + storage::MerchantAccountUpdate::ToPlatformAccount, + &key_store, + ) + .await + .change_context(errors::ApiErrorResponse::InternalServerError) + .attach_printable("Error while enabling platform merchant account") + .map(|_| services::ApplicationResponse::StatusOk) +} diff --git a/crates/router/src/core/fraud_check.rs b/crates/router/src/core/fraud_check.rs index 70c35b460e86..99a60817303a 100644 --- a/crates/router/src/core/fraud_check.rs +++ b/crates/router/src/core/fraud_check.rs @@ -588,6 +588,7 @@ pub async fn post_payment_frm_core<'a, F, D>( customer: &Option, key_store: domain::MerchantKeyStore, should_continue_capture: &mut bool, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult> where F: Send + Clone, @@ -647,6 +648,7 @@ where payment_data, customer, should_continue_capture, + platform_merchant_account, ) .await?; logger::debug!("frm_post_tasks_data: {:?}", frm_data); diff --git a/crates/router/src/core/fraud_check/operation.rs b/crates/router/src/core/fraud_check/operation.rs index d802339b675a..721afaa1e499 100644 --- a/crates/router/src/core/fraud_check/operation.rs +++ b/crates/router/src/core/fraud_check/operation.rs @@ -85,6 +85,7 @@ pub trait Domain: Send + Sync { _payment_data: &mut D, _customer: &Option, _should_continue_capture: &mut bool, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult> where F: Send + Clone, diff --git a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs index 308a00ffa1dc..69f06e594166 100644 --- a/crates/router/src/core/fraud_check/operation/fraud_check_post.rs +++ b/crates/router/src/core/fraud_check/operation/fraud_check_post.rs @@ -226,6 +226,7 @@ where _payment_data: &mut D, _customer: &Option, _should_continue_capture: &mut bool, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult> { todo!() } @@ -244,6 +245,7 @@ where payment_data: &mut D, customer: &Option, _should_continue_capture: &mut bool, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult> { if matches!(frm_data.fraud_check.frm_status, FraudCheckStatus::Fraud) && matches!( @@ -277,6 +279,7 @@ where payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + platform_merchant_account.cloned(), )) .await?; logger::debug!("payment_id : {:?} has been cancelled since it has been found fraudulent by configured frm connector",payment_data.get_payment_attempt().payment_id); @@ -334,6 +337,7 @@ where payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + platform_merchant_account.cloned(), )) .await?; logger::debug!("payment_id : {:?} has been captured since it has been found legit by configured frm connector",payment_data.get_payment_attempt().payment_id); diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index e68bd09ba462..535a6b72bd67 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -242,6 +242,7 @@ pub async fn payments_operation_core( auth_flow: services::AuthFlow, eligible_connectors: Option>, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResult<(D, Req, Option, Option, Option)> where F: Send + Clone + Sync, @@ -286,6 +287,7 @@ where &key_store, auth_flow, &header_payload, + platform_merchant_account.as_ref(), ) .await?; core_utils::validate_profile_id_from_auth_layer( @@ -716,6 +718,7 @@ where &customer, key_store.clone(), &mut should_continue_capture, + platform_merchant_account.as_ref(), )) .await?; } @@ -823,8 +826,8 @@ pub async fn proxy_for_payments_operation_core( req: Req, call_connector_action: CallConnectorAction, auth_flow: services::AuthFlow, - header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResult<(D, Req, Option, Option, Option)> where F: Send + Clone + Sync, @@ -869,6 +872,7 @@ where &key_store, auth_flow, &header_payload, + platform_merchant_account.as_ref(), ) .await?; @@ -1021,6 +1025,7 @@ pub async fn payments_intent_operation_core( req: Req, payment_id: id_type::GlobalPaymentId, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResult<(D, Req, Option)> where F: Send + Clone + Sync, @@ -1048,6 +1053,7 @@ where &profile, &key_store, &header_payload, + platform_merchant_account.as_ref(), ) .await?; @@ -1339,6 +1345,7 @@ pub async fn payments_core( call_connector_action: CallConnectorAction, eligible_connectors: Option>, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResponse where F: Send + Clone + Sync, @@ -1377,6 +1384,7 @@ where auth_flow, eligible_routable_connectors, header_payload.clone(), + platform_merchant_account, ) .await?; @@ -1406,6 +1414,7 @@ pub async fn proxy_for_payments_core( auth_flow: services::AuthFlow, call_connector_action: CallConnectorAction, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResponse where F: Send + Clone + Sync, @@ -1437,6 +1446,7 @@ where call_connector_action, auth_flow, header_payload.clone(), + platform_merchant_account, ) .await?; @@ -1465,6 +1475,7 @@ pub async fn payments_intent_core( req: Req, payment_id: id_type::GlobalPaymentId, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResponse where F: Send + Clone + Sync, @@ -1483,6 +1494,7 @@ where req, payment_id, header_payload.clone(), + platform_merchant_account, ) .await?; @@ -1556,6 +1568,7 @@ where &profile, &key_store, &header_payload, + None, ) .await?; @@ -1624,9 +1637,11 @@ pub trait PaymentRedirectFlow: Sync { connector_action: CallConnectorAction, connector: String, payment_id: id_type::PaymentId, + platform_merchant_account: Option, ) -> RouterResult; #[cfg(feature = "v2")] + #[allow(clippy::too_many_arguments)] async fn call_payment_flow( &self, state: &SessionState, @@ -1635,6 +1650,7 @@ pub trait PaymentRedirectFlow: Sync { merchant_key_store: domain::MerchantKeyStore, profile: domain::Profile, req: PaymentsRedirectResponseData, + platform_merchant_account: Option, ) -> RouterResult; fn get_payment_action(&self) -> services::PaymentAction; @@ -1661,6 +1677,7 @@ pub trait PaymentRedirectFlow: Sync { merchant_account: domain::MerchantAccount, key_store: domain::MerchantKeyStore, req: PaymentsRedirectResponseData, + platform_merchant_account: Option, ) -> RouterResponse { metrics::REDIRECTION_TRIGGERED.add( 1, @@ -1715,6 +1732,7 @@ pub trait PaymentRedirectFlow: Sync { flow_type, connector.clone(), resource_id.clone(), + platform_merchant_account, ) .await?; @@ -1722,6 +1740,7 @@ pub trait PaymentRedirectFlow: Sync { } #[cfg(feature = "v2")] + #[allow(clippy::too_many_arguments)] async fn handle_payments_redirect_response( &self, state: SessionState, @@ -1730,6 +1749,7 @@ pub trait PaymentRedirectFlow: Sync { key_store: domain::MerchantKeyStore, profile: domain::Profile, request: PaymentsRedirectResponseData, + platform_merchant_account: Option, ) -> RouterResponse { metrics::REDIRECTION_TRIGGERED.add( 1, @@ -1744,6 +1764,7 @@ pub trait PaymentRedirectFlow: Sync { key_store, profile, request, + platform_merchant_account, ) .await?; @@ -1770,6 +1791,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize { connector_action: CallConnectorAction, _connector: String, _payment_id: id_type::PaymentId, + platform_merchant_account: Option, ) -> RouterResult { let key_manager_state = &state.into(); @@ -1805,6 +1827,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize { connector_action, None, HeaderPayload::default(), + platform_merchant_account, )) .await?; let payments_response = match response { @@ -1910,6 +1933,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync { connector_action: CallConnectorAction, _connector: String, _payment_id: id_type::PaymentId, + platform_merchant_account: Option, ) -> RouterResult { let key_manager_state = &state.into(); @@ -1942,6 +1966,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync { connector_action, None, HeaderPayload::default(), + platform_merchant_account, ), ) .await?; @@ -2031,6 +2056,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync { merchant_key_store: domain::MerchantKeyStore, profile: domain::Profile, req: PaymentsRedirectResponseData, + platform_merchant_account: Option, ) -> RouterResult { let payment_id = req.payment_id.clone(); @@ -2057,6 +2083,7 @@ impl PaymentRedirectFlow for PaymentRedirectSync { &profile, &merchant_key_store, &HeaderPayload::default(), + platform_merchant_account.as_ref(), ) .await?; @@ -2168,6 +2195,7 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize { connector_action: CallConnectorAction, connector: String, payment_id: id_type::PaymentId, + platform_merchant_account: Option, ) -> RouterResult { let merchant_id = merchant_account.get_id().clone(); let key_manager_state = &state.into(); @@ -2267,6 +2295,7 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize { connector_action, None, HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator), + platform_merchant_account, )) .await? } else { @@ -2299,6 +2328,7 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize { connector_action, None, HeaderPayload::default(), + platform_merchant_account, ), ) .await? diff --git a/crates/router/src/core/payments/helpers.rs b/crates/router/src/core/payments/helpers.rs index c243686a76cd..eda869c3a6ca 100644 --- a/crates/router/src/core/payments/helpers.rs +++ b/crates/router/src/core/payments/helpers.rs @@ -3614,6 +3614,7 @@ mod tests { tax_details: None, skip_external_tax_calculation: None, psd2_sca_exemption_type: None, + platform_merchant_id: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_ok()); @@ -3684,6 +3685,7 @@ mod tests { tax_details: None, skip_external_tax_calculation: None, psd2_sca_exemption_type: None, + platform_merchant_id: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent,).is_err()) @@ -3752,6 +3754,7 @@ mod tests { tax_details: None, skip_external_tax_calculation: None, psd2_sca_exemption_type: None, + platform_merchant_id: None, }; let req_cs = Some("1".to_string()); assert!(authenticate_client_secret(req_cs.as_ref(), &payment_intent).is_err()) diff --git a/crates/router/src/core/payments/operations.rs b/crates/router/src/core/payments/operations.rs index 38911bb2d6df..4adb94034181 100644 --- a/crates/router/src/core/payments/operations.rs +++ b/crates/router/src/core/payments/operations.rs @@ -190,6 +190,7 @@ pub trait GetTracker: Send { mechant_key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>; #[cfg(feature = "v2")] @@ -203,6 +204,7 @@ pub trait GetTracker: Send { profile: &domain::Profile, mechant_key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>; } diff --git a/crates/router/src/core/payments/operations/payment_approve.rs b/crates/router/src/core/payments/operations/payment_approve.rs index a5993eb2f012..c830b7618d07 100644 --- a/crates/router/src/core/payments/operations/payment_approve.rs +++ b/crates/router/src/core/payments/operations/payment_approve.rs @@ -45,6 +45,7 @@ impl GetTracker, api::PaymentsCaptureR key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse<'a, F, api::PaymentsCaptureRequest, PaymentData>, > { diff --git a/crates/router/src/core/payments/operations/payment_cancel.rs b/crates/router/src/core/payments/operations/payment_cancel.rs index 427c10aab629..c6679e481f1b 100644 --- a/crates/router/src/core/payments/operations/payment_cancel.rs +++ b/crates/router/src/core/payments/operations/payment_cancel.rs @@ -46,6 +46,7 @@ impl GetTracker, api::PaymentsCancelRe key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse<'a, F, api::PaymentsCancelRequest, PaymentData>, > { diff --git a/crates/router/src/core/payments/operations/payment_capture.rs b/crates/router/src/core/payments/operations/payment_capture.rs index f8d304bcb79c..ebe49f59f649 100644 --- a/crates/router/src/core/payments/operations/payment_capture.rs +++ b/crates/router/src/core/payments/operations/payment_capture.rs @@ -45,6 +45,7 @@ impl GetTracker, api::Paymen key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse< 'a, diff --git a/crates/router/src/core/payments/operations/payment_capture_v2.rs b/crates/router/src/core/payments/operations/payment_capture_v2.rs index 0ee62ea73c1f..81ffa8e992fd 100644 --- a/crates/router/src/core/payments/operations/payment_capture_v2.rs +++ b/crates/router/src/core/payments/operations/payment_capture_v2.rs @@ -142,6 +142,7 @@ impl GetTracker, PaymentsCaptureReques _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_complete_authorize.rs b/crates/router/src/core/payments/operations/payment_complete_authorize.rs index 914ddf251efe..72c04c6a5497 100644 --- a/crates/router/src/core/payments/operations/payment_complete_authorize.rs +++ b/crates/router/src/core/payments/operations/payment_complete_authorize.rs @@ -48,6 +48,7 @@ impl GetTracker, api::PaymentsRequest> key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; diff --git a/crates/router/src/core/payments/operations/payment_confirm.rs b/crates/router/src/core/payments/operations/payment_confirm.rs index 96406932445a..c309685a6a29 100644 --- a/crates/router/src/core/payments/operations/payment_confirm.rs +++ b/crates/router/src/core/payments/operations/payment_confirm.rs @@ -77,6 +77,7 @@ impl GetTracker, api::PaymentsRequest> key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_confirm_intent.rs b/crates/router/src/core/payments/operations/payment_confirm_intent.rs index 374a33026da8..71d99d03e411 100644 --- a/crates/router/src/core/payments/operations/payment_confirm_intent.rs +++ b/crates/router/src/core/payments/operations/payment_confirm_intent.rs @@ -158,6 +158,7 @@ impl GetTracker, PaymentsConfir profile: &domain::Profile, key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_create.rs b/crates/router/src/core/payments/operations/payment_create.rs index 36b338f2d528..b7b3420987d1 100644 --- a/crates/router/src/core/payments/operations/payment_create.rs +++ b/crates/router/src/core/payments/operations/payment_create.rs @@ -78,6 +78,7 @@ impl GetTracker, api::PaymentsRequest> merchant_key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; @@ -304,6 +305,7 @@ impl GetTracker, api::PaymentsRequest> attempt_id, profile_id.clone(), session_expiry, + platform_merchant_account, ) .await?; @@ -1308,6 +1310,7 @@ impl PaymentCreate { active_attempt_id: String, profile_id: common_utils::id_type::ProfileId, session_expiry: PrimitiveDateTime, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult { let created_at @ modified_at @ last_synced = common_utils::date_time::now(); @@ -1494,6 +1497,8 @@ impl PaymentCreate { tax_details, skip_external_tax_calculation, psd2_sca_exemption_type: request.psd2_sca_exemption_type, + platform_merchant_id: platform_merchant_account + .map(|platform_merchant_account| platform_merchant_account.get_id().to_owned()), }) } diff --git a/crates/router/src/core/payments/operations/payment_create_intent.rs b/crates/router/src/core/payments/operations/payment_create_intent.rs index f9cdd192f18f..1cd0010bdf63 100644 --- a/crates/router/src/core/payments/operations/payment_create_intent.rs +++ b/crates/router/src/core/payments/operations/payment_create_intent.rs @@ -99,6 +99,7 @@ impl profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); @@ -137,6 +138,7 @@ impl profile, request.clone(), encrypted_data, + platform_merchant_account, ) .await?; diff --git a/crates/router/src/core/payments/operations/payment_get.rs b/crates/router/src/core/payments/operations/payment_get.rs index 403ee5443148..6419376f0902 100644 --- a/crates/router/src/core/payments/operations/payment_get.rs +++ b/crates/router/src/core/payments/operations/payment_get.rs @@ -119,6 +119,7 @@ impl GetTracker, PaymentsRetriev _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_get_intent.rs b/crates/router/src/core/payments/operations/payment_get_intent.rs index 16b44436c18f..3cc26e7b67f5 100644 --- a/crates/router/src/core/payments/operations/payment_get_intent.rs +++ b/crates/router/src/core/payments/operations/payment_get_intent.rs @@ -89,6 +89,7 @@ impl GetTracker, Payme _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs index 7337f0808847..0e189583d0a2 100644 --- a/crates/router/src/core/payments/operations/payment_post_session_tokens.rs +++ b/crates/router/src/core/payments/operations/payment_post_session_tokens.rs @@ -45,6 +45,7 @@ impl GetTracker, api::PaymentsPostSess key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse< 'a, diff --git a/crates/router/src/core/payments/operations/payment_reject.rs b/crates/router/src/core/payments/operations/payment_reject.rs index a6a10be8e9ab..e5321b1f9847 100644 --- a/crates/router/src/core/payments/operations/payment_reject.rs +++ b/crates/router/src/core/payments/operations/payment_reject.rs @@ -43,6 +43,7 @@ impl GetTracker, PaymentsCancelRequest key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; diff --git a/crates/router/src/core/payments/operations/payment_session.rs b/crates/router/src/core/payments/operations/payment_session.rs index f93327b275cf..0e94fbe09c6f 100644 --- a/crates/router/src/core/payments/operations/payment_session.rs +++ b/crates/router/src/core/payments/operations/payment_session.rs @@ -44,6 +44,7 @@ impl GetTracker, api::PaymentsSessionR key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse<'a, F, api::PaymentsSessionRequest, PaymentData>, > { diff --git a/crates/router/src/core/payments/operations/payment_session_intent.rs b/crates/router/src/core/payments/operations/payment_session_intent.rs index 2454b9fcbf31..ec59ba3473eb 100644 --- a/crates/router/src/core/payments/operations/payment_session_intent.rs +++ b/crates/router/src/core/payments/operations/payment_session_intent.rs @@ -101,6 +101,7 @@ impl GetTracker, Payme _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payment_start.rs b/crates/router/src/core/payments/operations/payment_start.rs index a895855cefc8..1da9a1a2264f 100644 --- a/crates/router/src/core/payments/operations/payment_start.rs +++ b/crates/router/src/core/payments/operations/payment_start.rs @@ -43,6 +43,7 @@ impl GetTracker, api::PaymentsStartReq key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse<'a, F, api::PaymentsStartRequest, PaymentData>, > { diff --git a/crates/router/src/core/payments/operations/payment_status.rs b/crates/router/src/core/payments/operations/payment_status.rs index 9aa905345c8c..dd28043ba2b8 100644 --- a/crates/router/src/core/payments/operations/payment_status.rs +++ b/crates/router/src/core/payments/operations/payment_status.rs @@ -213,6 +213,7 @@ impl GetTracker, api::PaymentsRetrieve key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse<'a, F, api::PaymentsRetrieveRequest, PaymentData>, > { diff --git a/crates/router/src/core/payments/operations/payment_update.rs b/crates/router/src/core/payments/operations/payment_update.rs index c75794dd17f9..0e60407aa7ca 100644 --- a/crates/router/src/core/payments/operations/payment_update.rs +++ b/crates/router/src/core/payments/operations/payment_update.rs @@ -56,6 +56,7 @@ impl GetTracker, api::PaymentsRequest> key_store: &domain::MerchantKeyStore, auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let (mut payment_intent, mut payment_attempt, currency): (_, _, storage_enums::Currency); diff --git a/crates/router/src/core/payments/operations/payment_update_intent.rs b/crates/router/src/core/payments/operations/payment_update_intent.rs index f8ce03d558e9..4782d237e211 100644 --- a/crates/router/src/core/payments/operations/payment_update_intent.rs +++ b/crates/router/src/core/payments/operations/payment_update_intent.rs @@ -135,6 +135,7 @@ impl GetTracker, PaymentsUpda _profile: &domain::Profile, key_store: &domain::MerchantKeyStore, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult>> { let db = &*state.store; let key_manager_state = &state.into(); diff --git a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs index 035c4e8e2ecf..2c816ad39d1d 100644 --- a/crates/router/src/core/payments/operations/payments_incremental_authorization.rs +++ b/crates/router/src/core/payments/operations/payments_incremental_authorization.rs @@ -48,6 +48,7 @@ impl key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse< 'a, diff --git a/crates/router/src/core/payments/operations/tax_calculation.rs b/crates/router/src/core/payments/operations/tax_calculation.rs index 859422920e16..5b9c90f3add5 100644 --- a/crates/router/src/core/payments/operations/tax_calculation.rs +++ b/crates/router/src/core/payments/operations/tax_calculation.rs @@ -50,6 +50,7 @@ impl key_store: &domain::MerchantKeyStore, _auth_flow: services::AuthFlow, _header_payload: &hyperswitch_domain_models::payments::HeaderPayload, + _platform_merchant_account: Option<&domain::MerchantAccount>, ) -> RouterResult< operations::GetTrackerResponse< 'a, diff --git a/crates/router/src/core/payments/session_operation.rs b/crates/router/src/core/payments/session_operation.rs index d7b6ad0d345b..40a2717fea72 100644 --- a/crates/router/src/core/payments/session_operation.rs +++ b/crates/router/src/core/payments/session_operation.rs @@ -42,6 +42,7 @@ pub async fn payments_session_core( payment_id: id_type::GlobalPaymentId, call_connector_action: CallConnectorAction, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResponse where F: Send + Clone + Sync, @@ -71,6 +72,7 @@ where payment_id, call_connector_action, header_payload.clone(), + platform_merchant_account, ) .await?; @@ -100,6 +102,7 @@ pub async fn payments_session_operation_core( payment_id: id_type::GlobalPaymentId, _call_connector_action: CallConnectorAction, header_payload: HeaderPayload, + platform_merchant_account: Option, ) -> RouterResult<(D, Req, Option, Option, Option)> where F: Send + Clone + Sync, @@ -132,6 +135,7 @@ where &profile, &key_store, &header_payload, + platform_merchant_account.as_ref(), ) .await?; diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 73d9cfdcaee6..ff9849958b51 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -617,6 +617,7 @@ async fn payments_incoming_webhook_flow( consume_or_trigger_flow.clone(), None, HeaderPayload::default(), + None, //Platform merchant account )) .await; // When mandate details are present in successful webhooks, and consuming webhooks are skipped during payment sync if the payment status is already updated to charged, this function is used to update the connector mandate details. @@ -1159,6 +1160,7 @@ async fn external_authentication_incoming_webhook_flow( payments::CallConnectorAction::Trigger, None, HeaderPayload::with_source(enums::PaymentSource::ExternalAuthenticator), + None, // Platform merchant account )) .await?; match payments_response { @@ -1356,6 +1358,7 @@ async fn frm_incoming_webhook_flow( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + None, // Platform merchant account )) .await? } @@ -1385,6 +1388,7 @@ async fn frm_incoming_webhook_flow( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + None, // Platform merchant account )) .await? } @@ -1543,6 +1547,7 @@ async fn bank_transfer_webhook_flow( payments::CallConnectorAction::Trigger, None, HeaderPayload::with_source(common_enums::PaymentSource::Webhook), + None, //Platform merchant account )) .await } else { diff --git a/crates/router/src/lib.rs b/crates/router/src/lib.rs index 625a4d9c95b5..9ed724c36ef0 100644 --- a/crates/router/src/lib.rs +++ b/crates/router/src/lib.rs @@ -90,6 +90,7 @@ pub mod headers { pub const X_REDIRECT_URI: &str = "x-redirect-uri"; pub const X_TENANT_ID: &str = "x-tenant-id"; pub const X_CLIENT_SECRET: &str = "X-Client-Secret"; + pub const X_CONNECTED_MERCHANT_ID: &str = "x-connected-merchant-id"; pub const X_RESOURCE_TYPE: &str = "X-Resource-Type"; } diff --git a/crates/router/src/routes/admin.rs b/crates/router/src/routes/admin.rs index a57fd56e6c84..9557aabbe81d 100644 --- a/crates/router/src/routes/admin.rs +++ b/crates/router/src/routes/admin.rs @@ -910,3 +910,26 @@ pub async fn merchant_account_transfer_keys( )) .await } + +/// Merchant Account - Platform Account +/// +/// Enable platform account +#[instrument(skip_all)] +pub async fn merchant_account_enable_platform_account( + state: web::Data, + req: HttpRequest, + path: web::Path, +) -> HttpResponse { + let flow = Flow::EnablePlatformAccount; + let merchant_id = path.into_inner(); + Box::pin(api::server_wrap( + flow, + state, + &req, + merchant_id, + |state, _, req, _| enable_platform_account(state, req), + &auth::AdminApiAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 2decddb806cb..5865c1014fb8 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1337,8 +1337,7 @@ impl MerchantAccount { #[cfg(all(feature = "olap", feature = "v1"))] impl MerchantAccount { pub fn server(state: AppState) -> Scope { - web::scope("/accounts") - .app_data(web::Data::new(state)) + let mut routes = web::scope("/accounts") .service(web::resource("").route(web::post().to(admin::merchant_account_create))) .service(web::resource("/list").route(web::get().to(admin::merchant_account_list))) .service( @@ -1358,7 +1357,14 @@ impl MerchantAccount { .route(web::get().to(admin::retrieve_merchant_account)) .route(web::post().to(admin::update_merchant_account)) .route(web::delete().to(admin::delete_merchant_account)), + ); + if state.conf.platform.enabled { + routes = routes.service( + web::resource("/{id}/platform") + .route(web::post().to(admin::merchant_account_enable_platform_account)), ) + } + routes.app_data(web::Data::new(state)) } } diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 4b43c42f3bc1..198692ac2d68 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -48,7 +48,8 @@ impl From for ApiIdentifier { | Flow::MerchantsAccountUpdate | Flow::MerchantsAccountDelete | Flow::MerchantTransferKey - | Flow::MerchantAccountList => Self::MerchantAccount, + | Flow::MerchantAccountList + | Flow::EnablePlatformAccount => Self::MerchantAccount, Flow::OrganizationCreate | Flow::OrganizationRetrieve | Flow::OrganizationUpdate => { Self::Organization diff --git a/crates/router/src/routes/payments.rs b/crates/router/src/routes/payments.rs index a9005644bebb..46f6a5e492ae 100644 --- a/crates/router/src/routes/payments.rs +++ b/crates/router/src/routes/payments.rs @@ -85,6 +85,7 @@ pub async fn payments_create( header_payload.clone(), req, api::AuthFlow::Merchant, + auth.platform_merchant_account, ) }, match env::which() { @@ -143,6 +144,7 @@ pub async fn payments_create_intent( req, global_payment_id.clone(), header_payload.clone(), + auth.platform_merchant_account, ) }, match env::which() { @@ -206,6 +208,7 @@ pub async fn payments_get_intent( req, global_payment_id.clone(), header_payload.clone(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -261,6 +264,7 @@ pub async fn payments_update_intent( req.payload, global_payment_id.clone(), header_payload.clone(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -316,6 +320,7 @@ pub async fn payments_start( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + None, ) }, &auth::MerchantIdAuth(merchant_id), @@ -390,6 +395,7 @@ pub async fn payments_retrieve( payments::CallConnectorAction::Trigger, None, header_payload.clone(), + auth.platform_merchant_account, ) }, auth::auth_type( @@ -460,6 +466,7 @@ pub async fn payments_retrieve_with_gateway_creds( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, &*auth_type, @@ -512,6 +519,7 @@ pub async fn payments_update( HeaderPayload::default(), req, auth_flow, + auth.platform_merchant_account, ) }, &*auth_type, @@ -570,6 +578,7 @@ pub async fn payments_post_session_tokens( payments::CallConnectorAction::Trigger, None, header_payload.clone(), + None, ) }, &auth::PublishableKeyAuth, @@ -632,6 +641,7 @@ pub async fn payments_confirm( header_payload.clone(), req, auth_flow, + auth.platform_merchant_account, ) }, &*auth_type, @@ -684,6 +694,7 @@ pub async fn payments_capture( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -743,6 +754,7 @@ pub async fn payments_dynamic_tax_calculation( payments::CallConnectorAction::Trigger, None, header_payload.clone(), + None, ) }, &auth::PublishableKeyAuth, @@ -806,6 +818,7 @@ pub async fn payments_connector_session( payment_id, payments::CallConnectorAction::Trigger, header_payload.clone(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::PublishableKeyAuth), @@ -864,6 +877,7 @@ pub async fn payments_connector_session( payments::CallConnectorAction::Trigger, None, header_payload.clone(), + None, ) }, &auth::HeaderAuth(auth::PublishableKeyAuth), @@ -913,7 +927,7 @@ pub async fn payments_redirect_response( auth.merchant_account, auth.key_store, req, - + auth.platform_merchant_account, ) }, &auth::MerchantIdAuth(merchant_id), @@ -963,7 +977,7 @@ pub async fn payments_redirect_response_with_creds_identifier( auth.merchant_account, auth.key_store, req, - + auth.platform_merchant_account, ) }, &auth::MerchantIdAuth(merchant_id), @@ -1014,7 +1028,7 @@ pub async fn payments_complete_authorize_redirect( auth.merchant_account, auth.key_store, req, - + auth.platform_merchant_account, ) }, &auth::MerchantIdAuth(merchant_id), @@ -1080,6 +1094,7 @@ pub async fn payments_complete_authorize( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + None, ) }, &*auth_type, @@ -1129,6 +1144,7 @@ pub async fn payments_cancel( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -1409,6 +1425,7 @@ pub async fn payments_approve( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, match env::which() { @@ -1473,6 +1490,7 @@ pub async fn payments_reject( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, match env::which() { @@ -1502,6 +1520,7 @@ async fn authorize_verify_select( header_payload: HeaderPayload, req: api_models::payments::PaymentsRequest, auth_flow: api::AuthFlow, + platform_merchant_account: Option, ) -> errors::RouterResponse where Op: Sync @@ -1551,6 +1570,7 @@ where auth_flow, payments::CallConnectorAction::Trigger, header_payload, + platform_merchant_account, ) .await } else { @@ -1578,6 +1598,7 @@ where payments::CallConnectorAction::Trigger, eligible_connectors, header_payload, + platform_merchant_account, ) .await } @@ -1601,6 +1622,7 @@ where payments::CallConnectorAction::Trigger, eligible_connectors, header_payload, + platform_merchant_account, ) .await } @@ -1649,6 +1671,7 @@ pub async fn payments_incremental_authorization( payments::CallConnectorAction::Trigger, None, HeaderPayload::default(), + auth.platform_merchant_account, ) }, &auth::HeaderAuth(auth::ApiKeyAuth), @@ -1734,6 +1757,7 @@ pub async fn post_3ds_payments_authorize( auth.merchant_account, auth.key_store, req, + auth.platform_merchant_account, ) }, &auth::MerchantIdAuth(merchant_id), @@ -2437,6 +2461,7 @@ pub async fn payments_finish_redirection( auth.key_store, auth.profile, req, + auth.platform_merchant_account ) }, &auth::PublishableKeyAndProfileIdAuth { diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index e5412c20fe29..d35e321a7bea 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -63,6 +63,7 @@ mod detached; #[derive(Clone, Debug)] pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, + pub platform_merchant_account: Option, pub key_store: domain::MerchantKeyStore, pub profile_id: Option, } @@ -73,6 +74,7 @@ pub struct AuthenticationData { pub merchant_account: domain::MerchantAccount, pub key_store: domain::MerchantKeyStore, pub profile: domain::Profile, + pub platform_merchant_account: Option, } #[derive(Clone, Debug)] @@ -466,6 +468,28 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + // Get connected merchant account if API call is done by Platform merchant account on behalf of connected merchant account + let (merchant, platform_merchant_account) = if state.conf().platform.enabled { + get_platform_merchant_account(state, request_headers, merchant).await? + } else { + (merchant, None) + }; + + let key_store = if platform_merchant_account.is_some() { + state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + merchant.get_id(), + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")? + } else { + key_store + }; + let profile = state .store() .find_business_profile_by_profile_id(key_manager_state, &key_store, &profile_id) @@ -474,6 +498,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account, key_store, profile, }; @@ -563,8 +588,31 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + // Get connected merchant account if API call is done by Platform merchant account on behalf of connected merchant account + let (merchant, platform_merchant_account) = if state.conf().platform.enabled { + get_platform_merchant_account(state, request_headers, merchant).await? + } else { + (merchant, None) + }; + + let key_store = if platform_merchant_account.is_some() { + state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + merchant.get_id(), + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")? + } else { + key_store + }; + let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account, key_store, profile_id, }; @@ -639,7 +687,8 @@ where merchant_id: Some(merchant_id), key_id: Some(key_id), } => { - let auth = construct_authentication_data(state, &merchant_id).await?; + let auth = + construct_authentication_data(state, &merchant_id, request_headers).await?; Ok(( auth.clone(), AuthenticationType::ApiKey { @@ -653,7 +702,8 @@ where merchant_id: Some(merchant_id), key_id: None, } => { - let auth = construct_authentication_data(state, &merchant_id).await?; + let auth = + construct_authentication_data(state, &merchant_id, request_headers).await?; Ok(( auth.clone(), AuthenticationType::PublishableKey { @@ -716,6 +766,7 @@ where let auth_data_v2 = AuthenticationData { merchant_account: auth_data.merchant_account, + platform_merchant_account: auth_data.platform_merchant_account, key_store: auth_data.key_store, profile, }; @@ -727,9 +778,10 @@ where async fn construct_authentication_data( state: &A, merchant_id: &id_type::MerchantId, + request_headers: &HeaderMap, ) -> RouterResult where - A: SessionStateInfo, + A: SessionStateInfo + Sync, { let key_store = state .store() @@ -752,8 +804,31 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + // Get connected merchant account if API call is done by Platform merchant account on behalf of connected merchant account + let (merchant, platform_merchant_account) = if state.conf().platform.enabled { + get_platform_merchant_account(state, request_headers, merchant).await? + } else { + (merchant, None) + }; + + let key_store = if platform_merchant_account.is_some() { + state + .store() + .get_merchant_key_store_by_merchant_id( + &(&state.session_state()).into(), + merchant.get_id(), + &state.store().get_master_key().to_vec().into(), + ) + .await + .change_context(errors::ApiErrorResponse::Unauthorized) + .attach_printable("Failed to fetch merchant key store for the merchant id")? + } else { + key_store + }; + let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account, key_store, profile_id: None, }; @@ -1003,6 +1078,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: None, }; @@ -1025,6 +1101,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + AdminApiAuth .authenticate_and_fetch(request_headers, state) .await?; @@ -1063,6 +1143,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( @@ -1084,6 +1165,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + AdminApiAuth .authenticate_and_fetch(request_headers, state) .await?; @@ -1182,6 +1267,34 @@ impl<'a> HeaderMapStruct<'a> { ) }) } + + pub fn get_id_type_from_header_if_present(&self, key: &str) -> RouterResult> + where + T: TryFrom< + std::borrow::Cow<'static, str>, + Error = error_stack::Report, + >, + { + self.headers + .get(key) + .map(|value| value.to_str()) + .transpose() + .change_context(errors::ApiErrorResponse::InvalidDataValue { + field_name: "`{key}` in headers", + }) + .attach_printable(format!( + "Failed to convert header value to string for header key: {}", + key + ))? + .map(|value| { + T::try_from(std::borrow::Cow::Owned(value.to_owned())).change_context( + errors::ApiErrorResponse::InvalidRequestData { + message: format!("`{}` header is invalid", key), + }, + ) + }) + .transpose() + } } /// Get the merchant-id from `x-merchant-id` header @@ -1225,6 +1338,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: None, }; @@ -1246,6 +1360,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + AdminApiAuth .authenticate_and_fetch(request_headers, state) .await?; @@ -1286,6 +1404,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth, @@ -1306,6 +1425,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationDataWithoutProfile, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + AdminApiAuth .authenticate_and_fetch(request_headers, state) .await?; @@ -1418,9 +1541,13 @@ where { async fn authenticate_and_fetch( &self, - _request_headers: &HeaderMap, + request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + let key_manager_state = &(&state.session_state()).into(); let key_store = state .store() @@ -1440,6 +1567,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: None, }; @@ -1463,6 +1591,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + let key_manager_state = &(&state.session_state()).into(); let profile_id = get_id_type_by_key_from_headers(headers::X_PROFILE_ID.to_string(), request_headers)? @@ -1497,6 +1629,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth.clone(), @@ -1525,6 +1658,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + let key_manager_state = &(&state.session_state()).into(); let key_store = state .store() @@ -1556,6 +1693,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth.clone(), @@ -1615,6 +1753,7 @@ where merchant_account, key_store, profile, + platform_merchant_account: None, }, AuthenticationType::PublishableKey { merchant_id }, )) @@ -1642,6 +1781,10 @@ where request_headers: &HeaderMap, state: &A, ) -> RouterResult<(AuthenticationData, AuthenticationType)> { + if state.conf().platform.enabled { + throw_error_if_platform_merchant_authentication_required(request_headers)?; + } + let publishable_key = get_api_key(request_headers).change_context(errors::ApiErrorResponse::Unauthorized)?; let key_manager_state = &(&state.session_state()).into(); @@ -1655,6 +1798,7 @@ where ( AuthenticationData { merchant_account, + platform_merchant_account: None, key_store, profile_id: None, }, @@ -1703,6 +1847,7 @@ where merchant_account, key_store, profile, + platform_merchant_account: None, }, AuthenticationType::PublishableKey { merchant_id }, )) @@ -1991,6 +2136,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -2073,6 +2219,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( @@ -2239,6 +2386,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -2314,6 +2462,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth.clone(), @@ -2448,6 +2597,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -2519,6 +2669,7 @@ where // if both of them are same then proceed with the profile id present in the request let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(self.profile_id.clone()), }; @@ -2592,6 +2743,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth.clone(), @@ -2680,6 +2832,7 @@ where let merchant_id = merchant.get_id().clone(); let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -2756,6 +2909,7 @@ where merchant_account: merchant, key_store, profile, + platform_merchant_account: None, }; Ok(( auth, @@ -2816,6 +2970,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -2934,6 +3089,7 @@ where let auth = AuthenticationData { merchant_account: merchant, + platform_merchant_account: None, key_store, profile_id: Some(payload.profile_id), }; @@ -3298,6 +3454,96 @@ where } } +async fn get_connected_merchant_account( + state: &A, + connected_merchant_id: id_type::MerchantId, + platform_org_id: id_type::OrganizationId, +) -> RouterResult +where + A: SessionStateInfo + Sync, +{ + let key_manager_state = &(&state.session_state()).into(); + let key_store = state + .store() + .get_merchant_key_store_by_merchant_id( + key_manager_state, + &connected_merchant_id, + &state.store().get_master_key().to_vec().into(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidPlatformOperation) + .attach_printable("Failed to fetch merchant key store for the merchant id")?; + + let connected_merchant_account = state + .store() + .find_merchant_account_by_merchant_id(key_manager_state, &connected_merchant_id, &key_store) + .await + .to_not_found_response(errors::ApiErrorResponse::InvalidPlatformOperation) + .attach_printable("Failed to fetch merchant account for the merchant id")?; + + if platform_org_id != connected_merchant_account.organization_id { + return Err(errors::ApiErrorResponse::InvalidPlatformOperation) + .attach_printable("Access for merchant id Unauthorized"); + } + + Ok(connected_merchant_account) +} + +async fn get_platform_merchant_account( + state: &A, + request_headers: &HeaderMap, + merchant_account: domain::MerchantAccount, +) -> RouterResult<(domain::MerchantAccount, Option)> +where + A: SessionStateInfo + Sync, +{ + let connected_merchant_id = + get_and_validate_connected_merchant_id(request_headers, &merchant_account)?; + + match connected_merchant_id { + Some(merchant_id) => { + let connected_merchant_account = get_connected_merchant_account( + state, + merchant_id, + merchant_account.organization_id.clone(), + ) + .await?; + Ok((connected_merchant_account, Some(merchant_account))) + } + None => Ok((merchant_account, None)), + } +} + +fn get_and_validate_connected_merchant_id( + request_headers: &HeaderMap, + merchant_account: &domain::MerchantAccount, +) -> RouterResult> { + HeaderMapStruct::new(request_headers) + .get_id_type_from_header_if_present::( + headers::X_CONNECTED_MERCHANT_ID, + )? + .map(|merchant_id| { + merchant_account + .is_platform_account + .then_some(merchant_id) + .ok_or(errors::ApiErrorResponse::InvalidPlatformOperation) + }) + .transpose() + .attach_printable("Non platform_merchant_account using X_CONNECTED_MERCHANT_ID header") +} + +fn throw_error_if_platform_merchant_authentication_required( + request_headers: &HeaderMap, +) -> RouterResult<()> { + HeaderMapStruct::new(request_headers) + .get_id_type_from_header_if_present::( + headers::X_CONNECTED_MERCHANT_ID, + )? + .map_or(Ok(()), |_| { + Err(errors::ApiErrorResponse::PlatformAccountAuthNotSupported.into()) + }) +} + #[cfg(feature = "recon")] #[async_trait] impl AuthenticateAndFetch for JWTAuth diff --git a/crates/router/src/utils/user/sample_data.rs b/crates/router/src/utils/user/sample_data.rs index 09cd7cfa1030..bf84dd568a2a 100644 --- a/crates/router/src/utils/user/sample_data.rs +++ b/crates/router/src/utils/user/sample_data.rs @@ -275,6 +275,7 @@ pub async fn generate_sample_data( tax_details: None, skip_external_tax_calculation: None, psd2_sca_exemption_type: None, + platform_merchant_id: None, }; let (connector_transaction_id, connector_transaction_data) = ConnectorTransactionId::form_id_and_data(attempt_id.clone()); diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index 1bfcc8ebe7bb..c7df4aeff0cd 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -400,6 +400,7 @@ async fn get_outgoing_webhook_content_and_event_type( CallConnectorAction::Avoid, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, //Platform merchant account )) .await? { diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 04fa16484922..fb83922935e3 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -91,6 +91,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { services::AuthFlow::Client, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, //Platform merchant account )) .await?; diff --git a/crates/router/tests/payments.rs b/crates/router/tests/payments.rs index 37fb57e05f34..beaacb79fc01 100644 --- a/crates/router/tests/payments.rs +++ b/crates/router/tests/payments.rs @@ -472,6 +472,7 @@ async fn payments_create_core() { payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, )) .await .unwrap(); @@ -734,6 +735,7 @@ async fn payments_create_core_adyen_no_redirect() { payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, )) .await .unwrap(); diff --git a/crates/router/tests/payments2.rs b/crates/router/tests/payments2.rs index 415ea07cb9f2..1d573d007ba6 100644 --- a/crates/router/tests/payments2.rs +++ b/crates/router/tests/payments2.rs @@ -234,6 +234,7 @@ async fn payments_create_core() { payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, )) .await .unwrap(); @@ -504,6 +505,7 @@ async fn payments_create_core_adyen_no_redirect() { payments::CallConnectorAction::Trigger, None, hyperswitch_domain_models::payments::HeaderPayload::default(), + None, )) .await .unwrap(); diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index de2577e6c34d..ccbf9598a560 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -88,6 +88,8 @@ pub enum Flow { ConfigKeyCreate, /// ConfigKey fetch flow. ConfigKeyFetch, + /// Enable platform account flow. + EnablePlatformAccount, /// ConfigKey Update flow. ConfigKeyUpdate, /// ConfigKey Delete flow. diff --git a/migrations/2024-12-03-072318_platform_merchant_account/down.sql b/migrations/2024-12-03-072318_platform_merchant_account/down.sql new file mode 100644 index 000000000000..342380e09333 --- /dev/null +++ b/migrations/2024-12-03-072318_platform_merchant_account/down.sql @@ -0,0 +1,4 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE merchant_account DROP COLUMN IF EXISTS is_platform_account; + +ALTER TABLE payment_intent DROP COLUMN IF EXISTS platform_merchant_id; diff --git a/migrations/2024-12-03-072318_platform_merchant_account/up.sql b/migrations/2024-12-03-072318_platform_merchant_account/up.sql new file mode 100644 index 000000000000..cd07e163d7c8 --- /dev/null +++ b/migrations/2024-12-03-072318_platform_merchant_account/up.sql @@ -0,0 +1,4 @@ +-- Your SQL goes here +ALTER TABLE merchant_account ADD COLUMN IF NOT EXISTS is_platform_account BOOL NOT NULL DEFAULT FALSE; + +ALTER TABLE payment_intent ADD COLUMN IF NOT EXISTS platform_merchant_id VARCHAR(64);