feat(vsaas): integrate onboarding flow for vertical saas (#7884)

This commit is contained in:
Apoorv Dixit
2025-05-12 19:19:42 +05:30
committed by GitHub
parent 57cb3a9ff0
commit cf34be1728
22 changed files with 376 additions and 33 deletions

View File

@ -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,
}),
)
}

View File

@ -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<user_api::PlatformAccountCreateResponse> {
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::<Vec<_>>();
@ -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::<Vec<_>>(),

View File

@ -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
}
})

View File

@ -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")

View File

@ -248,6 +248,7 @@ impl From<Flow> for ApiIdentifier {
| Flow::SwitchOrg
| Flow::SwitchMerchantV2
| Flow::SwitchProfile
| Flow::CreatePlatformAccount
| Flow::UserOrgMerchantCreate
| Flow::UserMerchantAccountCreate
| Flow::GenerateSampleData

View File

@ -259,6 +259,29 @@ pub async fn create_tenant_user(
.await
}
#[cfg(feature = "v1")]
pub async fn create_platform(
state: web::Data<AppState>,
req: HttpRequest,
json_payload: web::Json<user_api::PlatformAccountCreateRequest>,
) -> 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<AppState>,

View File

@ -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<user_api::SignUpWithMerchantIdRequest> for NewUserOrganization {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::SignUpWithMerchantIdRequest) -> UserResult<Self> {
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<user_api::SignUpWithMerchantIdRequest> for NewUserOrganization {
impl From<user_api::SignUpRequest> 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<user_api::SignUpRequest> for NewUserOrganization {
impl From<user_api::ConnectAccountRequest> 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<UserMerchantCreateRequestWithToken> 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<user_api::PlatformAccountCreateRequest> 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<InviteeUserRequestWithInvitedUserToken> 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<api_models::user::UserOrgMerchantCreateRequest>
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<UserMerchantCreateRequestWithToken> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: UserMerchantCreateRequestWithToken) -> UserResult<Self> {
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<UserMerchantCreateRequestWithToken> for NewUserMerchant {
}
}
impl TryFrom<user_api::PlatformAccountCreateRequest> for NewUserMerchant {
type Error = error_stack::Report<UserErrors>;
fn try_from(value: user_api::PlatformAccountCreateRequest) -> UserResult<Self> {
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,

View File

@ -1825,7 +1825,7 @@ impl ForeignFrom<api_models::organization::OrganizationNew>
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<api_models::organization::OrganizationCreateRequest>
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

View File

@ -293,11 +293,7 @@ pub fn create_merchant_account_request_for_org(
org: organization::Organization,
product_type: common_enums::MerchantProductType,
) -> UserResult<api_models::admin::MerchantAccountCreate> {
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<id_type::MerchantId> {
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())
}
}