diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index b67532a0bf..2dec1e1ce2 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -15171,6 +15171,14 @@ } ], "nullable": true + }, + "merchant_account_type": { + "allOf": [ + { + "$ref": "#/components/schemas/MerchantAccountRequestType" + } + ], + "nullable": true } }, "additionalProperties": false @@ -15257,6 +15265,13 @@ } } }, + "MerchantAccountRequestType": { + "type": "string", + "enum": [ + "standard", + "connected" + ] + }, "MerchantAccountResponse": { "type": "object", "required": [ diff --git a/config/config.example.toml b/config/config.example.toml index 10a9e0e052..a6505abc30 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -1089,4 +1089,8 @@ connector_names = "connector_names" # Comma-separated list of allowed connec [infra_values] cluster = "CLUSTER" # value of CLUSTER from deployment -version = "HOSTNAME" # value of HOSTNAME from deployment which tells its version \ No newline at end of file +version = "HOSTNAME" # value of HOSTNAME from deployment which tells its version + +[platform] +enabled = false # Enable or disable platform features +allow_connected_merchants = false # Enable or disable connected merchant account features \ No newline at end of file diff --git a/config/deployments/integration_test.toml b/config/deployments/integration_test.toml index 30c99e9f1b..10038b3aca 100644 --- a/config/deployments/integration_test.toml +++ b/config/deployments/integration_test.toml @@ -736,6 +736,7 @@ connector_list = "adyen,cybersource" [platform] enabled = true +allow_connected_merchants = true [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" diff --git a/config/deployments/production.toml b/config/deployments/production.toml index 33a787d86c..f747d1f451 100644 --- a/config/deployments/production.toml +++ b/config/deployments/production.toml @@ -746,6 +746,7 @@ connector_list = "cybersource" [platform] enabled = false +allow_connected_merchants = false [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" diff --git a/config/deployments/sandbox.toml b/config/deployments/sandbox.toml index 06123d9bf4..13624a12e7 100644 --- a/config/deployments/sandbox.toml +++ b/config/deployments/sandbox.toml @@ -752,6 +752,7 @@ connector_list = "adyen,cybersource" [platform] enabled = false +allow_connected_merchants = false [billing_connectors_payment_sync] billing_connectors_which_require_payment_sync = "stripebilling, recurly" diff --git a/config/development.toml b/config/development.toml index e2c1f7952e..a72dd2a161 100644 --- a/config/development.toml +++ b/config/development.toml @@ -1157,6 +1157,7 @@ background_color = "#FFFFFF" [platform] enabled = true +allow_connected_merchants = true [authentication_providers] click_to_pay = {connector_list = "adyen, cybersource"} diff --git a/config/docker_compose.toml b/config/docker_compose.toml index 5e64233dce..40ce9b8e68 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -1070,6 +1070,7 @@ background_color = "#FFFFFF" [platform] enabled = true +allow_connected_merchants = true [authentication_providers] click_to_pay = {connector_list = "adyen, cybersource"} diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 213691ef4f..e8669b454b 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -112,6 +112,10 @@ pub struct MerchantAccountCreate { /// Product Type of this merchant account #[schema(value_type = Option, example = "Orchestration")] pub product_type: Option, + + /// Merchant Account Type of this merchant account + #[schema(value_type = Option, example = "standard")] + pub merchant_account_type: Option, } #[cfg(feature = "v1")] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 4bc3c262c1..47608532b5 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -168,6 +168,7 @@ pub struct PlatformAccountCreateResponse { pub struct UserMerchantCreate { pub company_name: String, pub product_type: Option, + pub merchant_account_type: Option, } #[derive(serde::Serialize, Debug, Clone)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 49f1f019d0..9344f8cbbd 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,7 +6,9 @@ use std::{ num::{ParseFloatError, TryFromIntError}, }; -pub use accounts::{MerchantAccountType, MerchantProductType, OrganizationType}; +pub use accounts::{ + MerchantAccountRequestType, MerchantAccountType, MerchantProductType, OrganizationType, +}; pub use payments::ProductType; use serde::{Deserialize, Serialize}; pub use ui::*; diff --git a/crates/common_enums/src/enums/accounts.rs b/crates/common_enums/src/enums/accounts.rs index 46dae93466..2d575f4d83 100644 --- a/crates/common_enums/src/enums/accounts.rs +++ b/crates/common_enums/src/enums/accounts.rs @@ -70,3 +70,25 @@ pub enum OrganizationType { Standard, Platform, } + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, + ToSchema, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum MerchantAccountRequestType { + #[default] + Standard, + Connected, +} diff --git a/crates/openapi/src/openapi.rs b/crates/openapi/src/openapi.rs index d6af3ee036..d0585fd98c 100644 --- a/crates/openapi/src/openapi.rs +++ b/crates/openapi/src/openapi.rs @@ -477,6 +477,7 @@ Never share your secret api keys. Keep them guarded and secure. api_models::payments::ClickToPaySessionResponse, api_models::enums::ProductType, api_models::enums::MerchantAccountType, + api_models::enums::MerchantAccountRequestType, api_models::payments::GooglePayWalletData, api_models::payments::PayPalWalletData, api_models::payments::PaypalRedirection, diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index 9ce9106ce2..9424b2572d 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -191,6 +191,7 @@ pub struct CloneConnectorAllowlistConfig { #[derive(Debug, Deserialize, Clone, Default)] pub struct Platform { pub enabled: bool, + pub allow_connected_merchants: bool, } #[derive(Debug, Clone, Default, Deserialize)] @@ -1076,6 +1077,8 @@ impl Settings { .validate() .map_err(|err| ApplicationError::InvalidConfigurationValueError(err.to_string()))?; + self.platform.validate()?; + Ok(()) } } diff --git a/crates/router/src/configs/validations.rs b/crates/router/src/configs/validations.rs index f1df266500..1b8adfaa8b 100644 --- a/crates/router/src/configs/validations.rs +++ b/crates/router/src/configs/validations.rs @@ -305,3 +305,16 @@ impl super::settings::KeyManagerConfig { }) } } + +impl super::settings::Platform { + pub fn validate(&self) -> Result<(), ApplicationError> { + use common_utils::fp_utils::when; + + when(!self.enabled && self.allow_connected_merchants, || { + Err(ApplicationError::InvalidConfigurationValueError( + "platform.allow_connected_merchants cannot be true when platform.enabled is false" + .into(), + )) + }) + } +} diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 0a8af98dcc..94bf54a9f9 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -4,7 +4,7 @@ use api_models::{ admin::{self as admin_types}, enums as api_enums, routing as routing_types, }; -use common_enums::{MerchantAccountType, OrganizationType}; +use common_enums::{MerchantAccountRequestType, MerchantAccountType, OrganizationType}; use common_utils::{ date_time, ext_traits::{AsyncExt, Encode, OptionExt, ValueExt}, @@ -369,7 +369,20 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { .await?; let merchant_account_type = match organization.get_organization_type() { - OrganizationType::Standard => MerchantAccountType::Standard, + OrganizationType::Standard => { + match self.merchant_account_type.unwrap_or_default() { + // Allow only if explicitly Standard or not provided + MerchantAccountRequestType::Standard => MerchantAccountType::Standard, + MerchantAccountRequestType::Connected => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: + "Merchant account type must be Standard for a Standard Organization" + .to_string(), + } + .into()); + } + } + } OrganizationType::Platform => { let accounts = state @@ -385,10 +398,24 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { .iter() .any(|account| account.merchant_account_type == MerchantAccountType::Platform); - if platform_account_exists { - MerchantAccountType::Connected - } else { + if accounts.is_empty() || !platform_account_exists { + // First merchant in a Platform org must be Platform MerchantAccountType::Platform + } else { + match self.merchant_account_type.unwrap_or_default() { + MerchantAccountRequestType::Standard => MerchantAccountType::Standard, + MerchantAccountRequestType::Connected => { + if state.conf.platform.allow_connected_merchants { + MerchantAccountType::Connected + } else { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Connected merchant accounts are not allowed" + .to_string(), + } + .into()); + } + } + } } } }; diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index deab289f91..82c3e10105 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2281,7 +2281,6 @@ impl User { .service( web::resource("/tenant_signup").route(web::post().to(user::create_tenant_user)), ) - .service(web::resource("/create_platform").route(web::post().to(user::create_platform))) .service(web::resource("/create_org").route(web::post().to(user::user_org_create))) .service( web::resource("/create_merchant") @@ -2309,6 +2308,12 @@ impl User { .route(web::post().to(user::set_dashboard_metadata)), ); + if state.conf.platform.enabled { + route = route.service( + web::resource("/create_platform").route(web::post().to(user::create_platform)), + ) + } + route = route .service(web::scope("/key").service( web::resource("/transfer").route(web::post().to(user::transfer_user_key)), diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 35b7395038..ad854405a9 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -715,7 +715,9 @@ where ) -> RouterResult<(Option, AuthenticationType)> { // Step 1: Admin API Key and API Key Fallback (if allowed) if self.is_admin_auth_allowed { - let admin_auth = AdminApiAuthWithApiKeyFallback; + let admin_auth = AdminApiAuthWithApiKeyFallback { + organization_id: self.organization_id.clone(), + }; match admin_auth .authenticate_and_fetch(request_headers, state) .await @@ -1667,7 +1669,9 @@ where } #[derive(Debug, Default)] -pub struct AdminApiAuthWithApiKeyFallback; +pub struct AdminApiAuthWithApiKeyFallback { + pub organization_id: Option, +} #[cfg(feature = "v1")] #[async_trait] @@ -1742,6 +1746,16 @@ where .await .to_not_found_response(errors::ApiErrorResponse::Unauthorized)?; + if let Some(ref organization_id) = self.organization_id { + if organization_id != merchant.get_org_id() { + return Err( + report!(errors::ApiErrorResponse::Unauthorized).attach_printable( + "Organization ID from request and merchant account does not match", + ), + ); + } + } + if fallback_merchant_ids .merchant_ids .contains(&stored_api_key.merchant_id) diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 181d56d1cb..0ff4c388f7 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -416,6 +416,7 @@ pub struct NewUserMerchant { company_name: Option, new_organization: NewUserOrganization, product_type: Option, + merchant_account_type: Option, } impl TryFrom for MerchantName { @@ -508,6 +509,7 @@ impl NewUserMerchant { redirect_to_merchant_with_http_post: None, pm_collect_link_config: None, product_type: self.get_product_type(), + merchant_account_type: self.merchant_account_type, }) } @@ -520,7 +522,7 @@ impl NewUserMerchant { let merchant_account_create_request = self .create_merchant_account_request() - .attach_printable("unable to construct merchant account create request")?; + .attach_printable("Unable to construct merchant account create request")?; let org_id = merchant_account_create_request .clone() .organization_id @@ -535,7 +537,7 @@ impl NewUserMerchant { )) .await .change_context(UserErrors::InternalServerError) - .attach_printable("Error while creating a merchant")? + .attach_printable("Error while creating merchant")? else { return Err(UserErrors::InternalServerError.into()); }; @@ -643,6 +645,7 @@ impl TryFrom for NewUserMerchant { merchant_id, new_organization, product_type, + merchant_account_type: None, }) } } @@ -659,6 +662,7 @@ impl TryFrom for NewUserMerchant { merchant_id, new_organization, product_type, + merchant_account_type: None, }) } } @@ -670,12 +674,12 @@ impl TryFrom for NewUserMerchant { let merchant_id = MerchantId::new(value.company_name.clone())?; let new_organization = NewUserOrganization::try_from(value)?; let product_type = Some(consts::user::DEFAULT_PRODUCT_TYPE); - Ok(Self { company_name, merchant_id: id_type::MerchantId::try_from(merchant_id)?, new_organization, product_type, + merchant_account_type: None, }) } } @@ -696,6 +700,7 @@ impl TryFrom<(user_api::CreateInternalUserRequest, id_type::OrganizationId)> for merchant_id, new_organization, product_type: None, + merchant_account_type: None, }) } } @@ -710,6 +715,7 @@ impl TryFrom for NewUserMerchant { merchant_id, new_organization, product_type: None, + merchant_account_type: None, }) } } @@ -723,6 +729,7 @@ impl From<(user_api::CreateTenantUserRequest, MerchantAccountIdentifier)> for Ne merchant_id, new_organization, product_type: None, + merchant_account_type: None, } } } @@ -743,6 +750,7 @@ impl TryFrom for NewUserMerchant { user_merchant_create.company_name.clone(), )?), product_type: user_merchant_create.product_type, + merchant_account_type: user_merchant_create.merchant_account_type, new_organization: NewUserOrganization::from(( user_from_storage, user_merchant_create, @@ -766,6 +774,7 @@ impl TryFrom for NewUserMerchant { merchant_id, new_organization, product_type: Some(consts::user::DEFAULT_PRODUCT_TYPE), + merchant_account_type: None, }) } } diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 172f8bddcc..baeeb89d17 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -330,6 +330,7 @@ pub fn create_merchant_account_request_for_org( redirect_to_merchant_with_http_post: None, pm_collect_link_config: None, product_type: Some(product_type), + merchant_account_type: None, }) }