diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IResellerClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IResellerClientOrganizationSignUpCommand.cs
new file mode 100644
index 0000000000..01c8bff9a9
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IResellerClientOrganizationSignUpCommand.cs
@@ -0,0 +1,10 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Entities;
+using Bit.Core.Models.Business;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
+
+public interface IResellerClientOrganizationSignUpCommand
+{
+ Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup);
+}
diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs
new file mode 100644
index 0000000000..77b80c4cf9
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs
@@ -0,0 +1,234 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
+using Bit.Core.Billing.Pricing;
+using Bit.Core.Context;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Exceptions;
+using Bit.Core.Models.Business;
+using Bit.Core.Models.Data;
+using Bit.Core.Models.StaticStore;
+using Bit.Core.Platform.Push;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Tools.Enums;
+using Bit.Core.Tools.Models.Business;
+using Bit.Core.Tools.Services;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+
+public class ResellerClientOrganizationSignUpCommand : IResellerClientOrganizationSignUpCommand
+{
+ private readonly ICurrentContext _currentContext;
+ private readonly IPricingClient _pricingClient;
+ private readonly IReferenceEventService _referenceEventService;
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
+ private readonly IApplicationCacheService _applicationCacheService;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly IPushRegistrationService _pushRegistrationService;
+ private readonly IPushNotificationService _pushNotificationService;
+ private readonly ICollectionRepository _collectionRepository;
+ private readonly IDeviceRepository _deviceRepository;
+ private readonly IPaymentService _paymentService;
+
+ public ResellerClientOrganizationSignUpCommand(
+ ICurrentContext currentContext,
+ IPricingClient pricingClient,
+ IReferenceEventService referenceEventService,
+ IOrganizationRepository organizationRepository,
+ IOrganizationApiKeyRepository organizationApiKeyRepository,
+ IApplicationCacheService applicationCacheService,
+ IOrganizationUserRepository organizationUserRepository,
+ IPushRegistrationService pushRegistrationService,
+ IPushNotificationService pushNotificationService,
+ ICollectionRepository collectionRepository,
+ IDeviceRepository deviceRepository,
+ IPaymentService paymentService)
+ {
+ _currentContext = currentContext;
+ _pricingClient = pricingClient;
+ _referenceEventService = referenceEventService;
+ _organizationRepository = organizationRepository;
+ _organizationApiKeyRepository = organizationApiKeyRepository;
+ _applicationCacheService = applicationCacheService;
+ _organizationUserRepository = organizationUserRepository;
+ _pushRegistrationService = pushRegistrationService;
+ _pushNotificationService = pushNotificationService;
+ _collectionRepository = collectionRepository;
+ _deviceRepository = deviceRepository;
+ _paymentService = paymentService;
+ }
+
+ public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup)
+ {
+ var plan = await _pricingClient.GetPlanOrThrow(signup.Plan);
+
+ ValidatePlan(plan, signup.AdditionalSeats, "Password Manager");
+
+ var organization = new Organization
+ {
+ // Pre-generate the org id so that we can save it with the Stripe subscription.
+ Id = CoreHelpers.GenerateComb(),
+ Name = signup.Name,
+ BillingEmail = signup.BillingEmail,
+ PlanType = plan!.Type,
+ Seats = signup.AdditionalSeats,
+ MaxCollections = plan.PasswordManager.MaxCollections,
+ MaxStorageGb = 1,
+ UsePolicies = plan.HasPolicies,
+ UseSso = plan.HasSso,
+ UseGroups = plan.HasGroups,
+ UseEvents = plan.HasEvents,
+ UseDirectory = plan.HasDirectory,
+ UseTotp = plan.HasTotp,
+ Use2fa = plan.Has2fa,
+ UseApi = plan.HasApi,
+ UseResetPassword = plan.HasResetPassword,
+ SelfHost = plan.HasSelfHost,
+ UsersGetPremium = plan.UsersGetPremium,
+ UseCustomPermissions = plan.HasCustomPermissions,
+ UseScim = plan.HasScim,
+ Plan = plan.Name,
+ Gateway = GatewayType.Stripe,
+ ReferenceData = signup.Owner.ReferenceData,
+ Enabled = true,
+ LicenseKey = CoreHelpers.SecureRandomString(20),
+ PublicKey = signup.PublicKey,
+ PrivateKey = signup.PrivateKey,
+ CreationDate = DateTime.UtcNow,
+ RevisionDate = DateTime.UtcNow,
+ Status = OrganizationStatusType.Created,
+ UsePasswordManager = true,
+ // Secrets Manager not available for purchase with Consolidated Billing.
+ UseSecretsManager = false,
+ };
+
+ var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false);
+
+ await _referenceEventService.RaiseEventAsync(
+ new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext)
+ {
+ PlanName = plan.Name,
+ PlanType = plan.Type,
+ Seats = returnValue.Item1.Seats,
+ SignupInitiationPath = signup.InitiationPath,
+ Storage = returnValue.Item1.MaxStorageGb,
+ });
+
+ return returnValue;
+ }
+
+ private static void ValidatePlan(Plan plan, int additionalSeats, string productType)
+ {
+ if (plan is null)
+ {
+ throw new BadRequestException($"{productType} Plan was null.");
+ }
+
+ if (plan.Disabled)
+ {
+ throw new BadRequestException($"{productType} Plan not found.");
+ }
+
+ if (additionalSeats < 0)
+ {
+ throw new BadRequestException($"You can't subtract {productType} seats!");
+ }
+ }
+
+ ///
+ /// Private helper method to create a new organization.
+ /// This is common code used by both the cloud and self-hosted methods.
+ ///
+ private async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignUpAsync(Organization organization,
+ Guid ownerId, string ownerKey, string collectionName, bool withPayment)
+ {
+ try
+ {
+ await _organizationRepository.CreateAsync(organization);
+ await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey
+ {
+ OrganizationId = organization.Id,
+ ApiKey = CoreHelpers.SecureRandomString(30),
+ Type = OrganizationApiKeyType.Default,
+ RevisionDate = DateTime.UtcNow,
+ });
+ await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
+
+ // ownerId == default if the org is created by a provider - in this case it's created without an
+ // owner and the first owner is immediately invited afterwards
+ OrganizationUser orgUser = null;
+ if (ownerId != default)
+ {
+ orgUser = new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = ownerId,
+ Key = ownerKey,
+ AccessSecretsManager = organization.UseSecretsManager,
+ Type = OrganizationUserType.Owner,
+ Status = OrganizationUserStatusType.Confirmed,
+ CreationDate = organization.CreationDate,
+ RevisionDate = organization.CreationDate
+ };
+ orgUser.SetNewId();
+
+ await _organizationUserRepository.CreateAsync(orgUser);
+
+ var devices = await GetUserDeviceIdsAsync(orgUser.UserId.Value);
+ await _pushRegistrationService.AddUserRegistrationOrganizationAsync(devices,
+ organization.Id.ToString());
+ await _pushNotificationService.PushSyncOrgKeysAsync(ownerId);
+ }
+
+ Collection defaultCollection = null;
+ if (!string.IsNullOrWhiteSpace(collectionName))
+ {
+ defaultCollection = new Collection
+ {
+ Name = collectionName,
+ OrganizationId = organization.Id,
+ CreationDate = organization.CreationDate,
+ RevisionDate = organization.CreationDate
+ };
+
+ // Give the owner Can Manage access over the default collection
+ List defaultOwnerAccess = null;
+ if (orgUser != null)
+ {
+ defaultOwnerAccess =
+ [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }];
+ }
+
+ await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess);
+ }
+
+ return (organization, orgUser, defaultCollection);
+ }
+ catch
+ {
+ if (withPayment)
+ {
+ await _paymentService.CancelAndRecoverChargesAsync(organization);
+ }
+
+ if (organization.Id != default(Guid))
+ {
+ await _organizationRepository.DeleteAsync(organization);
+ await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
+ }
+
+ throw;
+ }
+ }
+
+ private async Task> GetUserDeviceIdsAsync(Guid userId)
+ {
+ var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
+ return devices
+ .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
+ .Select(d => d.Id.ToString());
+ }
+}