diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs
new file mode 100644
index 0000000000..48fc524769
--- /dev/null
+++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ResellerClientOrganizationSignUpCommand.cs
@@ -0,0 +1,129 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Core.Utilities;
+
+namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+
+public record ResellerClientOrganizationSignUpResponse(
+ Organization Organization,
+ OrganizationUser OwnerOrganizationUser);
+
+///
+/// Command for signing up reseller client organizations in a pending state.
+///
+public interface IResellerClientOrganizationSignUpCommand
+{
+ ///
+ /// Sign up a reseller client organization. The organization will be created in a pending state
+ /// (disabled and with Pending status) and the owner will be invited via email. The organization
+ /// will become active once the owner accepts the invitation.
+ ///
+ /// The organization to create.
+ /// The email of the organization owner who will be invited.
+ /// A response containing the created pending organization and invited owner user.
+ Task SignUpResellerClientAsync(
+ Organization organization,
+ string ownerEmail);
+}
+
+public class ResellerClientOrganizationSignUpCommand : IResellerClientOrganizationSignUpCommand
+{
+ private readonly IOrganizationRepository _organizationRepository;
+ private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
+ private readonly IApplicationCacheService _applicationCacheService;
+ private readonly IOrganizationUserRepository _organizationUserRepository;
+ private readonly IEventService _eventService;
+ private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand;
+ private readonly IPaymentService _paymentService;
+
+ public ResellerClientOrganizationSignUpCommand(
+ IOrganizationRepository organizationRepository,
+ IOrganizationApiKeyRepository organizationApiKeyRepository,
+ IApplicationCacheService applicationCacheService,
+ IOrganizationUserRepository organizationUserRepository,
+ IEventService eventService,
+ ISendOrganizationInvitesCommand sendOrganizationInvitesCommand,
+ IPaymentService paymentService)
+ {
+ _organizationRepository = organizationRepository;
+ _organizationApiKeyRepository = organizationApiKeyRepository;
+ _applicationCacheService = applicationCacheService;
+ _organizationUserRepository = organizationUserRepository;
+ _eventService = eventService;
+ _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand;
+ _paymentService = paymentService;
+ }
+
+ public async Task SignUpResellerClientAsync(
+ Organization organization,
+ string ownerEmail)
+ {
+ try
+ {
+ var createdOrganization = await CreateOrganizationAsync(organization);
+ var ownerOrganizationUser = await CreateAndInviteOwnerAsync(createdOrganization, ownerEmail);
+
+ await _eventService.LogOrganizationUserEventAsync(ownerOrganizationUser, EventType.OrganizationUser_Invited);
+
+ return new ResellerClientOrganizationSignUpResponse(organization, ownerOrganizationUser);
+ }
+ catch
+ {
+ await _paymentService.CancelAndRecoverChargesAsync(organization);
+
+ if (organization.Id != default)
+ {
+ await _organizationRepository.DeleteAsync(organization);
+ await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id);
+ }
+
+ throw;
+ }
+ }
+
+ private async Task CreateOrganizationAsync(Organization organization)
+ {
+ organization.Id = CoreHelpers.GenerateComb();
+ organization.Enabled = false;
+ organization.Status = OrganizationStatusType.Pending;
+
+ 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);
+
+ return organization;
+ }
+
+ private async Task CreateAndInviteOwnerAsync(Organization organization, string ownerEmail)
+ {
+ var ownerOrganizationUser = new OrganizationUser
+ {
+ OrganizationId = organization.Id,
+ UserId = null,
+ Email = ownerEmail,
+ Key = null,
+ Type = OrganizationUserType.Owner,
+ Status = OrganizationUserStatusType.Invited,
+ };
+
+ await _organizationUserRepository.CreateAsync(ownerOrganizationUser);
+
+ await _sendOrganizationInvitesCommand.SendInvitesAsync(new SendInvitesRequest(
+ users: [ownerOrganizationUser],
+ organization: organization,
+ initOrganization: true));
+
+ return ownerOrganizationUser;
+ }
+}
diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs
new file mode 100644
index 0000000000..55e5698ad4
--- /dev/null
+++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ResellerClientOrganizationSignUpCommandTests.cs
@@ -0,0 +1,185 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Repositories;
+using Bit.Core.Services;
+using Bit.Test.Common.AutoFixture;
+using Bit.Test.Common.AutoFixture.Attributes;
+using NSubstitute;
+using Xunit;
+
+namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp;
+
+[SutProviderCustomize]
+public class ResellerClientOrganizationSignUpCommandTests
+{
+ [Theory]
+ [BitAutoData]
+ public async Task SignUpResellerClientAsync_WithValidParameters_CreatesOrganizationSuccessfully(
+ Organization organization,
+ string ownerEmail,
+ SutProvider sutProvider)
+ {
+ var result = await sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail);
+
+ Assert.NotNull(result.Organization);
+ Assert.False(result.Organization.Enabled);
+ Assert.Equal(OrganizationStatusType.Pending, result.Organization.Status);
+ Assert.NotNull(result.OwnerOrganizationUser);
+ Assert.Equal(ownerEmail, result.OwnerOrganizationUser.Email);
+ Assert.Equal(OrganizationUserType.Owner, result.OwnerOrganizationUser.Type);
+ Assert.Equal(OrganizationUserStatusType.Invited, result.OwnerOrganizationUser.Status);
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(
+ Arg.Is(o =>
+ o.Id != default &&
+ o.Name == organization.Name &&
+ o.Enabled == false &&
+ o.Status == OrganizationStatusType.Pending
+ )
+ );
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(
+ Arg.Is(k =>
+ k.OrganizationId == result.Organization.Id &&
+ k.Type == OrganizationApiKeyType.Default &&
+ !string.IsNullOrEmpty(k.ApiKey)
+ )
+ );
+ await sutProvider.GetDependency()
+ .Received(1)
+ .UpsertOrganizationAbilityAsync(Arg.Is(o => o.Id == result.Organization.Id));
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(
+ Arg.Is(u =>
+ u.OrganizationId == result.Organization.Id &&
+ u.Email == ownerEmail &&
+ u.Type == OrganizationUserType.Owner &&
+ u.Status == OrganizationUserStatusType.Invited &&
+ u.UserId == null
+ )
+ );
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SendInvitesAsync(
+ Arg.Is(r =>
+ r.Users.Count() == 1 &&
+ r.Users.First().Email == ownerEmail &&
+ r.Organization.Id == result.Organization.Id &&
+ r.InitOrganization == true
+ )
+ );
+ await sutProvider.GetDependency()
+ .Received(1)
+ .LogOrganizationUserEventAsync(
+ Arg.Is(u => u.Email == ownerEmail),
+ EventType.OrganizationUser_Invited
+ );
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SignUpResellerClientAsync_WhenOrganizationRepositoryThrows_PerformsCleanup(
+ Organization organization,
+ string ownerEmail,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency()
+ .When(x => x.CreateAsync(Arg.Any()))
+ .Do(_ => throw new Exception());
+
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));
+
+ await AssertCleanupIsPerformed(sutProvider);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SignUpResellerClientAsync_WhenOrganizationUserCreationFails_PerformsCleanup(
+ Organization organization,
+ string ownerEmail,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency()
+ .When(x => x.CreateAsync(Arg.Any()))
+ .Do(_ => throw new Exception());
+
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Any());
+ await AssertCleanupIsPerformed(sutProvider);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SignUpResellerClientAsync_WhenInvitationSendingFails_PerformsCleanup(
+ Organization organization,
+ string ownerEmail,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency()
+ .When(x => x.SendInvitesAsync(Arg.Any()))
+ .Do(_ => throw new Exception());
+
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Any());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Any());
+ await AssertCleanupIsPerformed(sutProvider);
+ }
+
+ [Theory]
+ [BitAutoData]
+ public async Task SignUpResellerClientAsync_WhenEventLoggingFails_PerformsCleanup(
+ Organization organization,
+ string ownerEmail,
+ SutProvider sutProvider)
+ {
+ sutProvider.GetDependency()
+ .When(x => x.LogOrganizationUserEventAsync(Arg.Any(), Arg.Any()))
+ .Do(_ => throw new Exception());
+
+ await Assert.ThrowsAsync(
+ () => sutProvider.Sut.SignUpResellerClientAsync(organization, ownerEmail));
+
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Any());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CreateAsync(Arg.Any());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .SendInvitesAsync(Arg.Any());
+ await AssertCleanupIsPerformed(sutProvider);
+ }
+
+ private static async Task AssertCleanupIsPerformed(SutProvider sutProvider)
+ {
+ await sutProvider.GetDependency()
+ .Received(1)
+ .CancelAndRecoverChargesAsync(Arg.Any());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .DeleteAsync(Arg.Any());
+ await sutProvider.GetDependency()
+ .Received(1)
+ .DeleteOrganizationAbilityAsync(Arg.Any());
+ }
+}