From cf34be17286d8722e66c0c379e0113957d1ab8eb Mon Sep 17 00:00:00 2001 From: Apoorv Dixit <64925866+apoorvdixit88@users.noreply.github.com> Date: Mon, 12 May 2025 19:19:42 +0530 Subject: [PATCH] feat(vsaas): integrate onboarding flow for vertical saas (#7884) --- crates/api_models/src/events/user.rs | 16 +++-- crates/api_models/src/organization.rs | 8 ++- crates/api_models/src/user.rs | 16 +++++ crates/common_enums/src/enums.rs | 2 +- crates/common_enums/src/enums/accounts.rs | 43 ++++++++++++ crates/diesel_models/src/merchant_account.rs | 10 +++ crates/diesel_models/src/organization.rs | 47 ++++++++++++- crates/diesel_models/src/schema.rs | 6 ++ crates/diesel_models/src/schema_v2.rs | 6 ++ .../src/merchant_account.rs | 13 ++++ crates/router/src/core/admin.rs | 46 ++++++++++++- crates/router/src/core/user.rs | 68 +++++++++++++++++++ crates/router/src/db/organization.rs | 2 + crates/router/src/routes/app.rs | 1 + crates/router/src/routes/lock_utils.rs | 1 + crates/router/src/routes/user.rs | 23 +++++++ crates/router/src/types/domain/user.rs | 63 +++++++++++++---- crates/router/src/types/transformers.rs | 10 ++- crates/router/src/utils/user.rs | 15 ++-- crates/router_env/src/logger/types.rs | 2 + .../down.sql | 6 ++ .../up.sql | 5 ++ 22 files changed, 376 insertions(+), 33 deletions(-) create mode 100644 migrations/2025-04-10-095823_add_platform_context_in_organization/down.sql create mode 100644 migrations/2025-04-10-095823_add_platform_context_in_organization/up.sql diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 5e0564b418..1a20e7b90b 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -15,13 +15,13 @@ use crate::user::{ CreateTenantUserRequest, CreateUserAuthenticationMethodRequest, ForgotPasswordRequest, GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse, GetUserRoleDetailsRequest, GetUserRoleDetailsResponseV2, InviteUserRequest, - ReInviteUserRequest, RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, - SendVerifyEmailRequest, SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest, - SwitchMerchantRequest, SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse, - TwoFactorAuthStatusResponse, TwoFactorStatus, UpdateUserAccountDetailsRequest, - UpdateUserAuthenticationMethodRequest, UserFromEmailRequest, UserMerchantAccountResponse, - UserMerchantCreate, UserOrgMerchantCreateRequest, VerifyEmailRequest, - VerifyRecoveryCodeRequest, VerifyTotpRequest, + PlatformAccountCreateRequest, PlatformAccountCreateResponse, ReInviteUserRequest, + RecoveryCodes, ResetPasswordRequest, RotatePasswordRequest, SendVerifyEmailRequest, + SignUpRequest, SignUpWithMerchantIdRequest, SsoSignInRequest, SwitchMerchantRequest, + SwitchOrganizationRequest, SwitchProfileRequest, TokenResponse, TwoFactorAuthStatusResponse, + TwoFactorStatus, UpdateUserAccountDetailsRequest, UpdateUserAuthenticationMethodRequest, + UserFromEmailRequest, UserMerchantAccountResponse, UserMerchantCreate, + UserOrgMerchantCreateRequest, VerifyEmailRequest, VerifyRecoveryCodeRequest, VerifyTotpRequest, }; common_utils::impl_api_event_type!( @@ -39,6 +39,8 @@ common_utils::impl_api_event_type!( SwitchProfileRequest, CreateInternalUserRequest, CreateTenantUserRequest, + PlatformAccountCreateRequest, + PlatformAccountCreateResponse, UserOrgMerchantCreateRequest, UserMerchantAccountResponse, UserMerchantCreate, diff --git a/crates/api_models/src/organization.rs b/crates/api_models/src/organization.rs index c6bc3924d1..e7ec5d32d7 100644 --- a/crates/api_models/src/organization.rs +++ b/crates/api_models/src/organization.rs @@ -1,14 +1,17 @@ +use common_enums::OrganizationType; use common_utils::{id_type, pii}; use utoipa::ToSchema; pub struct OrganizationNew { pub org_id: id_type::OrganizationId, + pub org_type: OrganizationType, pub org_name: Option, } impl OrganizationNew { - pub fn new(org_name: Option) -> Self { + pub fn new(org_type: OrganizationType, org_name: Option) -> Self { Self { org_id: id_type::OrganizationId::default(), + org_type, org_name, } } @@ -47,6 +50,9 @@ pub struct OrganizationUpdateRequest { /// Metadata is useful for storing additional, unstructured information on an object. #[schema(value_type = Option)] pub metadata: Option, + + /// Platform merchant id is unique distiguisher for special merchant in the platform org + pub platform_merchant_id: Option, } #[cfg(feature = "v1")] #[derive(Debug, serde::Serialize, Clone, ToSchema)] diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index aa39877f0c..d979a2d5a1 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -130,6 +130,20 @@ pub struct UserOrgMerchantCreateRequest { pub merchant_name: Secret, } +#[derive(Debug, serde::Deserialize, serde::Serialize, Clone)] +pub struct PlatformAccountCreateRequest { + pub organization_name: Secret, +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct PlatformAccountCreateResponse { + pub org_id: id_type::OrganizationId, + pub org_name: Option, + pub org_type: common_enums::OrganizationType, + pub merchant_id: id_type::MerchantId, + pub merchant_account_type: common_enums::MerchantAccountType, +} + #[derive(Debug, serde::Deserialize, serde::Serialize)] pub struct UserMerchantCreate { pub company_name: String, @@ -381,6 +395,7 @@ pub struct UserTransferKeyResponse { pub struct ListOrgsForUserResponse { pub org_id: id_type::OrganizationId, pub org_name: Option, + pub org_type: common_enums::OrganizationType, } #[derive(Debug, serde::Serialize)] @@ -388,6 +403,7 @@ pub struct UserMerchantAccountResponse { pub merchant_id: id_type::MerchantId, pub merchant_name: OptionalEncryptableName, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, pub version: common_enums::ApiVersion, } diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 89f375b51a..69707bf3ce 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -6,7 +6,7 @@ use std::{ num::{ParseFloatError, TryFromIntError}, }; -pub use accounts::MerchantProductType; +pub use accounts::{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 e5b3ef733c..7500dcac7c 100644 --- a/crates/common_enums/src/enums/accounts.rs +++ b/crates/common_enums/src/enums/accounts.rs @@ -26,3 +26,46 @@ pub enum MerchantProductType { CostObservability, DynamicRouting, } + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum MerchantAccountType { + #[default] + Standard, + Platform, + Connected, +} + +#[derive( + Clone, + Copy, + Debug, + Default, + Eq, + PartialEq, + serde::Deserialize, + serde::Serialize, + strum::Display, + strum::EnumString, +)] +#[router_derive::diesel_enum(storage_type = "text")] +#[strum(serialize_all = "snake_case")] +#[serde(rename_all = "snake_case")] +pub enum OrganizationType { + #[default] + Standard, + Platform, +} diff --git a/crates/diesel_models/src/merchant_account.rs b/crates/diesel_models/src/merchant_account.rs index 40c6a1e4ff..e0b421f40c 100644 --- a/crates/diesel_models/src/merchant_account.rs +++ b/crates/diesel_models/src/merchant_account.rs @@ -54,6 +54,7 @@ pub struct MerchantAccount { pub is_platform_account: bool, pub id: Option, pub product_type: Option, + pub merchant_account_type: Option, } #[cfg(feature = "v1")] @@ -88,6 +89,7 @@ pub struct MerchantAccountSetter { pub version: common_enums::ApiVersion, pub is_platform_account: bool, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v1")] @@ -125,6 +127,7 @@ impl From for MerchantAccount { version: item.version, is_platform_account: item.is_platform_account, product_type: item.product_type, + merchant_account_type: Some(item.merchant_account_type), } } } @@ -158,6 +161,7 @@ pub struct MerchantAccount { pub is_platform_account: bool, pub id: common_utils::id_type::MerchantId, pub product_type: Option, + pub merchant_account_type: Option, } #[cfg(feature = "v2")] @@ -177,6 +181,7 @@ impl From for MerchantAccount { version: item.version, is_platform_account: item.is_platform_account, product_type: item.product_type, + merchant_account_type: Some(item.merchant_account_type), } } } @@ -196,6 +201,7 @@ pub struct MerchantAccountSetter { pub version: common_enums::ApiVersion, pub is_platform_account: bool, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } impl MerchantAccount { @@ -245,6 +251,7 @@ pub struct MerchantAccountNew { pub is_platform_account: bool, pub id: Option, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v2")] @@ -263,6 +270,7 @@ pub struct MerchantAccountNew { pub version: common_enums::ApiVersion, pub is_platform_account: bool, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v2")] @@ -311,6 +319,7 @@ impl MerchantAccountUpdateInternal { id: source.id, is_platform_account: is_platform_account.unwrap_or(source.is_platform_account), product_type: product_type.or(source.product_type), + merchant_account_type: source.merchant_account_type, } } } @@ -417,6 +426,7 @@ impl MerchantAccountUpdateInternal { is_platform_account: is_platform_account.unwrap_or(source.is_platform_account), id: source.id, product_type: product_type.or(source.product_type), + merchant_account_type: source.merchant_account_type, } } } diff --git a/crates/diesel_models/src/organization.rs b/crates/diesel_models/src/organization.rs index 1ca8622593..ffa1e94fcf 100644 --- a/crates/diesel_models/src/organization.rs +++ b/crates/diesel_models/src/organization.rs @@ -29,6 +29,8 @@ pub struct Organization { #[allow(dead_code)] organization_name: Option, pub version: common_enums::ApiVersion, + pub organization_type: Option, + pub platform_merchant_id: Option, } #[cfg(feature = "v2")] @@ -46,6 +48,8 @@ pub struct Organization { id: id_type::OrganizationId, organization_name: Option, pub version: common_enums::ApiVersion, + pub organization_type: Option, + pub platform_merchant_id: Option, } #[cfg(feature = "v1")] @@ -61,6 +65,8 @@ impl Organization { id: _, organization_name: _, version, + organization_type, + platform_merchant_id, } = org_new; Self { id: Some(org_id.clone()), @@ -72,8 +78,14 @@ impl Organization { created_at, modified_at, version, + organization_type: Some(organization_type), + platform_merchant_id, } } + + pub fn get_organization_type(&self) -> common_enums::OrganizationType { + self.organization_type.unwrap_or_default() + } } #[cfg(feature = "v2")] @@ -87,6 +99,8 @@ impl Organization { created_at, modified_at, version, + organization_type, + platform_merchant_id, } = org_new; Self { id, @@ -96,8 +110,14 @@ impl Organization { created_at, modified_at, version, + organization_type: Some(organization_type), + platform_merchant_id, } } + + pub fn get_organization_type(&self) -> common_enums::OrganizationType { + self.organization_type.unwrap_or_default() + } } #[cfg(feature = "v1")] @@ -113,6 +133,8 @@ pub struct OrganizationNew { pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub version: common_enums::ApiVersion, + pub organization_type: common_enums::OrganizationType, + pub platform_merchant_id: Option, } #[cfg(feature = "v2")] @@ -126,11 +148,17 @@ pub struct OrganizationNew { pub created_at: time::PrimitiveDateTime, pub modified_at: time::PrimitiveDateTime, pub version: common_enums::ApiVersion, + pub organization_type: common_enums::OrganizationType, + pub platform_merchant_id: Option, } #[cfg(feature = "v1")] impl OrganizationNew { - pub fn new(id: id_type::OrganizationId, organization_name: Option) -> Self { + pub fn new( + id: id_type::OrganizationId, + organization_type: common_enums::OrganizationType, + organization_name: Option, + ) -> Self { Self { org_id: id.clone(), org_name: organization_name.clone(), @@ -141,13 +169,19 @@ impl OrganizationNew { created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), version: common_types::consts::API_VERSION, + organization_type, + platform_merchant_id: None, } } } #[cfg(feature = "v2")] impl OrganizationNew { - pub fn new(id: id_type::OrganizationId, organization_name: Option) -> Self { + pub fn new( + id: id_type::OrganizationId, + organization_type: common_enums::OrganizationType, + organization_name: Option, + ) -> Self { Self { id, organization_name, @@ -156,6 +190,8 @@ impl OrganizationNew { created_at: common_utils::date_time::now(), modified_at: common_utils::date_time::now(), version: common_types::consts::API_VERSION, + organization_type, + platform_merchant_id: None, } } } @@ -169,6 +205,7 @@ pub struct OrganizationUpdateInternal { organization_details: Option, metadata: Option, modified_at: time::PrimitiveDateTime, + platform_merchant_id: Option, } #[cfg(feature = "v2")] @@ -179,6 +216,7 @@ pub struct OrganizationUpdateInternal { organization_details: Option, metadata: Option, modified_at: time::PrimitiveDateTime, + platform_merchant_id: Option, } pub enum OrganizationUpdate { @@ -186,6 +224,7 @@ pub enum OrganizationUpdate { organization_name: Option, organization_details: Option, metadata: Option, + platform_merchant_id: Option, }, } @@ -197,12 +236,14 @@ impl From for OrganizationUpdateInternal { organization_name, organization_details, metadata, + platform_merchant_id, } => Self { org_name: organization_name.clone(), organization_name, organization_details, metadata, modified_at: common_utils::date_time::now(), + platform_merchant_id, }, } } @@ -216,11 +257,13 @@ impl From for OrganizationUpdateInternal { organization_name, organization_details, metadata, + platform_merchant_id, } => Self { organization_name, organization_details, metadata, modified_at: common_utils::date_time::now(), + platform_merchant_id, }, } } diff --git a/crates/diesel_models/src/schema.rs b/crates/diesel_models/src/schema.rs index c25acc386f..46a36bab8c 100644 --- a/crates/diesel_models/src/schema.rs +++ b/crates/diesel_models/src/schema.rs @@ -750,6 +750,8 @@ diesel::table! { id -> Nullable, #[max_length = 64] product_type -> Nullable, + #[max_length = 64] + merchant_account_type -> Nullable, } } @@ -823,6 +825,10 @@ diesel::table! { id -> Nullable, organization_name -> Nullable, version -> ApiVersion, + #[max_length = 64] + organization_type -> 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 8f857a4db2..d9df67f0fb 100644 --- a/crates/diesel_models/src/schema_v2.rs +++ b/crates/diesel_models/src/schema_v2.rs @@ -738,6 +738,8 @@ diesel::table! { id -> Varchar, #[max_length = 64] product_type -> Nullable, + #[max_length = 64] + merchant_account_type -> Nullable, } } @@ -800,6 +802,10 @@ diesel::table! { id -> Varchar, organization_name -> Nullable, version -> ApiVersion, + #[max_length = 64] + organization_type -> Nullable, + #[max_length = 64] + platform_merchant_id -> Nullable, } } diff --git a/crates/hyperswitch_domain_models/src/merchant_account.rs b/crates/hyperswitch_domain_models/src/merchant_account.rs index 8657af91c5..2cd1af9863 100644 --- a/crates/hyperswitch_domain_models/src/merchant_account.rs +++ b/crates/hyperswitch_domain_models/src/merchant_account.rs @@ -49,6 +49,7 @@ pub struct MerchantAccount { pub version: common_enums::ApiVersion, pub is_platform_account: bool, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v1")] @@ -85,6 +86,7 @@ pub struct MerchantAccountSetter { pub version: common_enums::ApiVersion, pub is_platform_account: bool, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v1")] @@ -121,6 +123,7 @@ impl From for MerchantAccount { version: item.version, is_platform_account: item.is_platform_account, product_type: item.product_type, + merchant_account_type: item.merchant_account_type, } } } @@ -142,6 +145,7 @@ pub struct MerchantAccountSetter { pub is_platform_account: bool, pub version: common_enums::ApiVersion, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } #[cfg(feature = "v2")] @@ -161,6 +165,7 @@ impl From for MerchantAccount { is_platform_account, version, product_type, + merchant_account_type, } = item; Self { id, @@ -176,6 +181,7 @@ impl From for MerchantAccount { is_platform_account, version, product_type, + merchant_account_type, } } } @@ -196,6 +202,7 @@ pub struct MerchantAccount { pub is_platform_account: bool, pub version: common_enums::ApiVersion, pub product_type: Option, + pub merchant_account_type: common_enums::MerchantAccountType, } impl MerchantAccount { @@ -575,6 +582,7 @@ impl super::behaviour::Conversion for MerchantAccount { version: common_types::consts::API_VERSION, is_platform_account: self.is_platform_account, product_type: self.product_type, + merchant_account_type: self.merchant_account_type, }; Ok(diesel_models::MerchantAccount::from(setter)) @@ -637,6 +645,7 @@ impl super::behaviour::Conversion for MerchantAccount { is_platform_account: item.is_platform_account, version: item.version, product_type: item.product_type, + merchant_account_type: item.merchant_account_type.unwrap_or_default(), }) } .await @@ -662,6 +671,7 @@ impl super::behaviour::Conversion for MerchantAccount { product_type: self .product_type .or(Some(common_enums::MerchantProductType::Orchestration)), + merchant_account_type: self.merchant_account_type, }) } } @@ -703,6 +713,7 @@ impl super::behaviour::Conversion for MerchantAccount { version: self.version, is_platform_account: self.is_platform_account, product_type: self.product_type, + merchant_account_type: self.merchant_account_type, }; Ok(diesel_models::MerchantAccount::from(setter)) @@ -782,6 +793,7 @@ impl super::behaviour::Conversion for MerchantAccount { version: item.version, is_platform_account: item.is_platform_account, product_type: item.product_type, + merchant_account_type: item.merchant_account_type.unwrap_or_default(), }) } .await @@ -825,6 +837,7 @@ impl super::behaviour::Conversion for MerchantAccount { product_type: self .product_type .or(Some(common_enums::MerchantProductType::Orchestration)), + merchant_account_type: self.merchant_account_type, }) } } diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 04ea9ab573..097e2c7bcf 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -4,6 +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_utils::{ date_time, ext_traits::{AsyncExt, Encode, OptionExt, ValueExt}, @@ -144,6 +145,7 @@ pub async fn update_organization( organization_name: req.organization_name, organization_details: req.organization_details, metadata: req.metadata, + platform_merchant_id: req.platform_merchant_id, }; state .accounts_store @@ -342,6 +344,31 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { .create_or_validate(db) .await?; + let merchant_account_type = match organization.get_organization_type() { + OrganizationType::Standard => MerchantAccountType::Standard, + + OrganizationType::Platform => { + let accounts = state + .store + .list_merchant_accounts_by_organization_id( + &state.into(), + &organization.get_organization_id(), + ) + .await + .to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)?; + + let platform_account_exists = accounts + .iter() + .any(|account| account.merchant_account_type == MerchantAccountType::Platform); + + if platform_account_exists { + MerchantAccountType::Connected + } else { + MerchantAccountType::Platform + } + } + }; + let key = key_store.key.clone().into_inner(); let key_manager_state = state.into(); @@ -411,6 +438,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { version: common_types::consts::API_VERSION, is_platform_account: false, product_type: self.product_type, + merchant_account_type, }, ) } @@ -467,7 +495,10 @@ impl CreateOrValidateOrganization { match self { #[cfg(feature = "v1")] Self::Create => { - let new_organization = api_models::organization::OrganizationNew::new(None); + let new_organization = api_models::organization::OrganizationNew::new( + OrganizationType::Standard, + None, + ); let db_organization = ForeignFrom::foreign_from(new_organization); db.insert_organization(db_organization) .await @@ -635,6 +666,18 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { .create_or_validate(db) .await?; + let merchant_account_type = match organization.get_organization_type() { + OrganizationType::Standard => MerchantAccountType::Standard, + // Blocking v2 merchant account create for platform + OrganizationType::Platform => { + return Err(errors::ApiErrorResponse::InvalidRequestData { + message: "Merchant account creation is not allowed for a platform organization" + .to_string(), + } + .into()) + } + }; + let key = key_store.key.into_inner(); let id = identifier.to_owned(); let key_manager_state = state.into(); @@ -681,6 +724,7 @@ impl MerchantAccountCreateBridge for api::MerchantAccountCreate { is_platform_account: false, version: common_types::consts::API_VERSION, product_type: self.product_type, + merchant_account_type, }), ) } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 6e5360d884..ff90ca1dfa 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -1497,6 +1497,71 @@ pub async fn create_tenant_user( Ok(ApplicationResponse::StatusOk) } +#[cfg(feature = "v1")] +pub async fn create_platform_account( + state: SessionState, + user_from_token: auth::UserFromToken, + req: user_api::PlatformAccountCreateRequest, +) -> UserResponse { + let user_from_db = user_from_token.get_user_from_db(&state).await?; + + let new_merchant = domain::NewUserMerchant::try_from(req)?; + let new_organization = new_merchant.get_new_organization(); + let organization = new_organization.insert_org_in_db(state.clone()).await?; + + let merchant_account = new_merchant + .create_new_merchant_and_insert_in_db(state.to_owned()) + .await?; + + state + .accounts_store + .update_organization_by_org_id( + &organization.get_organization_id(), + diesel_models::organization::OrganizationUpdate::Update { + organization_name: None, + organization_details: None, + metadata: None, + platform_merchant_id: Some(merchant_account.get_id().to_owned()), + }, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let now = common_utils::date_time::now(); + + let user_role = domain::NewUserRole { + user_id: user_from_db.get_user_id().to_owned(), + role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + status: UserStatus::Active, + created_by: user_from_token.user_id.clone(), + last_modified_by: user_from_token.user_id.clone(), + created_at: now, + last_modified: now, + entity: domain::NoLevel, + }; + + user_role + .add_entity(domain::OrganizationLevel { + tenant_id: user_from_token + .tenant_id + .clone() + .unwrap_or(state.tenant.tenant_id.clone()), + org_id: merchant_account.organization_id.clone(), + }) + .insert_in_v2(&state) + .await?; + + Ok(ApplicationResponse::Json( + user_api::PlatformAccountCreateResponse { + org_id: organization.get_organization_id(), + org_name: organization.get_organization_name(), + org_type: organization.organization_type.unwrap_or_default(), + merchant_id: merchant_account.get_id().to_owned(), + merchant_account_type: merchant_account.merchant_account_type, + }, + )) +} + #[cfg(feature = "v1")] pub async fn create_org_merchant_for_user( state: SessionState, @@ -1537,6 +1602,7 @@ pub async fn create_merchant_account( merchant_id: domain_merchant_account.get_id().to_owned(), merchant_name: domain_merchant_account.merchant_name, product_type: domain_merchant_account.product_type, + merchant_account_type: domain_merchant_account.merchant_account_type, version: domain_merchant_account.version, }, )) @@ -2893,6 +2959,7 @@ pub async fn list_orgs_for_user( .map(|org| user_api::ListOrgsForUserResponse { org_id: org.get_organization_id(), org_name: org.get_organization_name(), + org_type: org.organization_type.unwrap_or_default(), }) .collect::>(); @@ -2975,6 +3042,7 @@ pub async fn list_merchants_for_user_in_org( merchant_name: merchant_account.merchant_name.clone(), merchant_id: merchant_account.get_id().to_owned(), product_type: merchant_account.product_type, + merchant_account_type: merchant_account.merchant_account_type, version: merchant_account.version, }) .collect::>(), diff --git a/crates/router/src/db/organization.rs b/crates/router/src/db/organization.rs index b1dc58f040..bb4f125db2 100644 --- a/crates/router/src/db/organization.rs +++ b/crates/router/src/db/organization.rs @@ -119,12 +119,14 @@ impl OrganizationInterface for super::MockDb { organization_name, organization_details, metadata, + platform_merchant_id, } => { organization_name .as_ref() .map(|org_name| org.set_organization_name(org_name.to_owned())); organization_details.clone_into(&mut org.organization_details); metadata.clone_into(&mut org.metadata); + platform_merchant_id.clone_into(&mut org.platform_merchant_id); org } }) diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 842416e7c1..11e6349113 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -2193,6 +2193,7 @@ 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") diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index 53d7e3075f..fee72601df 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -248,6 +248,7 @@ impl From for ApiIdentifier { | Flow::SwitchOrg | Flow::SwitchMerchantV2 | Flow::SwitchProfile + | Flow::CreatePlatformAccount | Flow::UserOrgMerchantCreate | Flow::UserMerchantAccountCreate | Flow::GenerateSampleData diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index d075048ddb..a6d9d75dc4 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -259,6 +259,29 @@ pub async fn create_tenant_user( .await } +#[cfg(feature = "v1")] +pub async fn create_platform( + state: web::Data, + req: HttpRequest, + json_payload: web::Json, +) -> HttpResponse { + let flow = Flow::CreatePlatformAccount; + Box::pin(api::server_wrap( + flow, + state, + &req, + json_payload.into_inner(), + |state, user: auth::UserFromToken, json_payload, _| { + user_core::create_platform_account(state, user, json_payload) + }, + &auth::JWTAuth { + permission: Permission::OrganizationAccountWrite, + }, + api_locking::LockAction::NotApplicable, + )) + .await +} + #[cfg(feature = "v1")] pub async fn user_org_create( state: web::Data, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 3e0ddb971c..ca48d424d2 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -23,7 +23,7 @@ use hyperswitch_domain_models::api::ApplicationResponse; use masking::{ExposeInterface, PeekInterface, Secret}; use once_cell::sync::Lazy; use rand::distributions::{Alphanumeric, DistString}; -use router_env::{env, logger}; +use router_env::logger; use time::PrimitiveDateTime; use unicode_segmentation::UnicodeSegmentation; #[cfg(feature = "keymanager_create")] @@ -267,9 +267,10 @@ impl NewUserOrganization { impl TryFrom for NewUserOrganization { type Error = error_stack::Report; fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult { - let new_organization = api_org::OrganizationNew::new(Some( - UserCompanyName::new(value.company_name)?.get_secret(), - )); + let new_organization = api_org::OrganizationNew::new( + common_enums::OrganizationType::Standard, + Some(UserCompanyName::new(value.company_name)?.get_secret()), + ); let db_organization = ForeignFrom::foreign_from(new_organization); Ok(Self(db_organization)) } @@ -277,7 +278,8 @@ impl TryFrom for NewUserOrganization { impl From for NewUserOrganization { fn from(_value: user_api::SignUpRequest) -> Self { - let new_organization = api_org::OrganizationNew::new(None); + let new_organization = + api_org::OrganizationNew::new(common_enums::OrganizationType::Standard, None); let db_organization = ForeignFrom::foreign_from(new_organization); Self(db_organization) } @@ -285,7 +287,8 @@ impl From for NewUserOrganization { impl From for NewUserOrganization { fn from(_value: user_api::ConnectAccountRequest) -> Self { - let new_organization = api_org::OrganizationNew::new(None); + let new_organization = + api_org::OrganizationNew::new(common_enums::OrganizationType::Standard, None); let db_organization = ForeignFrom::foreign_from(new_organization); Self(db_organization) } @@ -297,6 +300,7 @@ impl From<(user_api::CreateInternalUserRequest, id_type::OrganizationId)> for Ne ) -> Self { let new_organization = api_org::OrganizationNew { org_id, + org_type: common_enums::OrganizationType::Standard, org_name: None, }; let db_organization = ForeignFrom::foreign_from(new_organization); @@ -308,15 +312,28 @@ impl From for NewUserOrganization { fn from(value: UserMerchantCreateRequestWithToken) -> Self { Self(diesel_org::OrganizationNew::new( value.2.org_id, + common_enums::OrganizationType::Standard, Some(value.1.company_name), )) } } +impl From for NewUserOrganization { + fn from(value: user_api::PlatformAccountCreateRequest) -> Self { + let new_organization = api_org::OrganizationNew::new( + common_enums::OrganizationType::Platform, + Some(value.organization_name.expose()), + ); + let db_organization = ForeignFrom::foreign_from(new_organization); + Self(db_organization) + } +} + type InviteeUserRequestWithInvitedUserToken = (user_api::InviteUserRequest, UserFromToken); impl From for NewUserOrganization { fn from(_value: InviteeUserRequestWithInvitedUserToken) -> Self { - let new_organization = api_org::OrganizationNew::new(None); + let new_organization = + api_org::OrganizationNew::new(common_enums::OrganizationType::Standard, None); let db_organization = ForeignFrom::foreign_from(new_organization); Self(db_organization) } @@ -331,6 +348,7 @@ impl From<(user_api::CreateTenantUserRequest, MerchantAccountIdentifier)> for Ne ) -> Self { let new_organization = api_org::OrganizationNew { org_id: merchant_account_identifier.org_id, + org_type: common_enums::OrganizationType::Standard, org_name: None, }; let db_organization = ForeignFrom::foreign_from(new_organization); @@ -349,7 +367,11 @@ impl ForeignFrom metadata, .. } = item; - let mut org_new_db = Self::new(org_id, Some(organization_name.expose())); + let mut org_new_db = Self::new( + org_id, + common_enums::OrganizationType::Standard, + Some(organization_name.expose()), + ); org_new_db.organization_details = organization_details; org_new_db.metadata = metadata; org_new_db @@ -702,11 +724,8 @@ impl TryFrom for NewUserMerchant { type Error = error_stack::Report; fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult { - let merchant_id = if matches!(env::which(), env::Env::Production) { - id_type::MerchantId::try_from(MerchantId::new(value.1.company_name.clone())?)? - } else { - id_type::MerchantId::new_from_unix_timestamp() - }; + let merchant_id = + utils::user::generate_env_specific_merchant_id(value.1.company_name.clone())?; let (user_from_storage, user_merchant_create, user_from_token) = value; Ok(Self { merchant_id, @@ -723,6 +742,24 @@ impl TryFrom for NewUserMerchant { } } +impl TryFrom for NewUserMerchant { + type Error = error_stack::Report; + + fn try_from(value: user_api::PlatformAccountCreateRequest) -> UserResult { + let merchant_id = utils::user::generate_env_specific_merchant_id( + value.organization_name.clone().expose(), + )?; + + let new_organization = NewUserOrganization::from(value); + Ok(Self { + company_name: None, + merchant_id, + new_organization, + product_type: Some(consts::user::DEFAULT_PRODUCT_TYPE), + }) + } +} + #[derive(Debug, Clone)] pub struct MerchantAccountIdentifier { pub merchant_id: id_type::MerchantId, diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 43ac6882bc..f6b7491274 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1825,7 +1825,7 @@ impl ForeignFrom for diesel_models::organization::OrganizationNew { fn foreign_from(item: api_models::organization::OrganizationNew) -> Self { - Self::new(item.org_id, item.org_name) + Self::new(item.org_id, item.org_type, item.org_name) } } @@ -1833,13 +1833,17 @@ impl ForeignFrom for diesel_models::organization::OrganizationNew { fn foreign_from(item: api_models::organization::OrganizationCreateRequest) -> Self { - let org_new = api_models::organization::OrganizationNew::new(None); + // Create a new organization with a standard type by default + let org_new = api_models::organization::OrganizationNew::new( + common_enums::OrganizationType::Standard, + None, + ); let api_models::organization::OrganizationCreateRequest { organization_name, organization_details, metadata, } = item; - let mut org_new_db = Self::new(org_new.org_id, Some(organization_name)); + let mut org_new_db = Self::new(org_new.org_id, org_new.org_type, Some(organization_name)); org_new_db.organization_details = organization_details; org_new_db.metadata = metadata; org_new_db diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index bd5237c38c..0c5fd187f9 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -293,11 +293,7 @@ pub fn create_merchant_account_request_for_org( org: organization::Organization, product_type: common_enums::MerchantProductType, ) -> UserResult { - let merchant_id = if matches!(env::which(), env::Env::Production) { - id_type::MerchantId::try_from(domain::MerchantId::new(req.merchant_name.clone().expose())?)? - } else { - id_type::MerchantId::new_from_unix_timestamp() - }; + let merchant_id = generate_env_specific_merchant_id(req.merchant_name.clone().expose())?; let company_name = domain::UserCompanyName::new(req.merchant_name.expose())?; Ok(api_models::admin::MerchantAccountCreate { @@ -390,3 +386,12 @@ pub async fn set_lineage_context_in_cache( Ok(()) } + +pub fn generate_env_specific_merchant_id(value: String) -> UserResult { + if matches!(env::which(), env::Env::Production) { + let raw_id = domain::MerchantId::new(value)?; + Ok(id_type::MerchantId::try_from(raw_id)?) + } else { + Ok(id_type::MerchantId::new_from_unix_timestamp()) + } +} diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 6d57291a60..bea51e808b 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -409,6 +409,8 @@ pub enum Flow { UpdateUserRole, /// Create merchant account for user in a org UserMerchantAccountCreate, + /// Create Platform + CreatePlatformAccount, /// Create Org in a given tenancy UserOrgMerchantCreate, /// Generate Sample Data diff --git a/migrations/2025-04-10-095823_add_platform_context_in_organization/down.sql b/migrations/2025-04-10-095823_add_platform_context_in_organization/down.sql new file mode 100644 index 0000000000..65379a8022 --- /dev/null +++ b/migrations/2025-04-10-095823_add_platform_context_in_organization/down.sql @@ -0,0 +1,6 @@ +-- This file should undo anything in `up.sql` +ALTER TABLE organization DROP COLUMN IF EXISTS organization_type; +ALTER TABLE organization DROP COLUMN IF EXISTS platform_merchant_id; + +ALTER TABLE merchant_account DROP COLUMN IF EXISTS merchant_account_type; + diff --git a/migrations/2025-04-10-095823_add_platform_context_in_organization/up.sql b/migrations/2025-04-10-095823_add_platform_context_in_organization/up.sql new file mode 100644 index 0000000000..cbcabee8e0 --- /dev/null +++ b/migrations/2025-04-10-095823_add_platform_context_in_organization/up.sql @@ -0,0 +1,5 @@ +-- Your SQL goes here +ALTER TABLE organization ADD COLUMN IF NOT EXISTS organization_type VARCHAR(64); +ALTER TABLE organization ADD COLUMN IF NOT EXISTS platform_merchant_id VARCHAR(64); + +ALTER TABLE merchant_account ADD COLUMN IF NOT EXISTS merchant_account_type VARCHAR(64);