using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing; using Bit.Core.Billing.Commands.Implementations; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Queries; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.Models.StaticStore; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; using static Bit.Core.Test.Billing.Utilities; namespace Bit.Core.Test.Billing.Commands; [SutProviderCustomize] public class AssignSeatsToClientOrganizationCommandTests { [Theory, BitAutoData] public Task AssignSeatsToClientOrganization_NullProvider_ArgumentNullException( Organization organization, int seats, SutProvider sutProvider) => Assert.ThrowsAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(null, organization, seats)); [Theory, BitAutoData] public Task AssignSeatsToClientOrganization_NullOrganization_ArgumentNullException( Provider provider, int seats, SutProvider sutProvider) => Assert.ThrowsAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, null, seats)); [Theory, BitAutoData] public Task AssignSeatsToClientOrganization_NegativeSeats_BillingException( Provider provider, Organization organization, SutProvider sutProvider) => Assert.ThrowsAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, -5)); [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_CurrentSeatsMatchesNewSeats_NoOp( Provider provider, Organization organization, int seats, SutProvider sutProvider) { organization.PlanType = PlanType.TeamsMonthly; organization.Seats = seats; await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); await sutProvider.GetDependency().DidNotReceive().GetByProviderId(provider.Id); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_OrganizationPlanTypeDoesNotSupportConsolidatedBilling_ContactSupport( Provider provider, Organization organization, int seats, SutProvider sutProvider) { organization.PlanType = PlanType.FamiliesAnnually; await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_ProviderPlanIsNotConfigured_ContactSupport( Provider provider, Organization organization, int seats, SutProvider sutProvider) { organization.PlanType = PlanType.TeamsMonthly; sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(new List { new () { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id } }); await ThrowsContactSupportAsync(() => sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats)); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_BelowToBelow_Succeeds( Provider provider, Organization organization, SutProvider sutProvider) { organization.Seats = 10; organization.PlanType = PlanType.TeamsMonthly; // Scale up 10 seats const int seats = 20; var providerPlans = new List { new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, PurchasedSeats = 0, // 100 minimum SeatMinimum = 100, AllocatedSeats = 50 }, new() { Id = Guid.NewGuid(), PlanType = PlanType.EnterpriseMonthly, ProviderId = provider.Id, PurchasedSeats = 0, SeatMinimum = 500, AllocatedSeats = 0 } }; var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 50 seats currently assigned with a seat minimum of 100 sutProvider.GetDependency().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(50); await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); // 50 assigned seats + 10 seat scale up = 60 seats, well below the 100 minimum await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().AdjustSeats( Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( org => org.Id == organization.Id && org.Seats == seats)); await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.AllocatedSeats == 60)); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_BelowToAbove_Succeeds( Provider provider, Organization organization, SutProvider sutProvider) { organization.Seats = 10; organization.PlanType = PlanType.TeamsMonthly; // Scale up 10 seats const int seats = 20; var providerPlans = new List { new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, PurchasedSeats = 0, // 100 minimum SeatMinimum = 100, AllocatedSeats = 95 }, new() { Id = Guid.NewGuid(), PlanType = PlanType.EnterpriseMonthly, ProviderId = provider.Id, PurchasedSeats = 0, SeatMinimum = 500, AllocatedSeats = 0 } }; var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 95 seats currently assigned with a seat minimum of 100 sutProvider.GetDependency().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(95); await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); // 95 current + 10 seat scale = 105 seats, 5 above the minimum await sutProvider.GetDependency().Received(1).AdjustSeats( provider, StaticStore.GetPlan(providerPlan.PlanType), providerPlan.SeatMinimum!.Value, 105); await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( org => org.Id == organization.Id && org.Seats == seats)); // 105 total seats - 100 minimum = 5 purchased seats await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 5 && pPlan.AllocatedSeats == 105)); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_AboveToAbove_Succeeds( Provider provider, Organization organization, SutProvider sutProvider) { organization.Seats = 10; organization.PlanType = PlanType.TeamsMonthly; // Scale up 10 seats const int seats = 20; var providerPlans = new List { new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, // 10 additional purchased seats PurchasedSeats = 10, // 100 seat minimum SeatMinimum = 100, AllocatedSeats = 110 }, new() { Id = Guid.NewGuid(), PlanType = PlanType.EnterpriseMonthly, ProviderId = provider.Id, PurchasedSeats = 0, SeatMinimum = 500, AllocatedSeats = 0 } }; var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 110 seats currently assigned with a seat minimum of 100 sutProvider.GetDependency().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110); await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); // 110 current + 10 seat scale up = 120 seats await sutProvider.GetDependency().Received(1).AdjustSeats( provider, StaticStore.GetPlan(providerPlan.PlanType), 110, 120); await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( org => org.Id == organization.Id && org.Seats == seats)); // 120 total seats - 100 seat minimum = 20 purchased seats await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 20 && pPlan.AllocatedSeats == 120)); } [Theory, BitAutoData] public async Task AssignSeatsToClientOrganization_AboveToBelow_Succeeds( Provider provider, Organization organization, SutProvider sutProvider) { organization.Seats = 50; organization.PlanType = PlanType.TeamsMonthly; // Scale down 30 seats const int seats = 20; var providerPlans = new List { new() { Id = Guid.NewGuid(), PlanType = PlanType.TeamsMonthly, ProviderId = provider.Id, // 10 additional purchased seats PurchasedSeats = 10, // 100 seat minimum SeatMinimum = 100, AllocatedSeats = 110 }, new() { Id = Guid.NewGuid(), PlanType = PlanType.EnterpriseMonthly, ProviderId = provider.Id, PurchasedSeats = 0, SeatMinimum = 500, AllocatedSeats = 0 } }; var providerPlan = providerPlans.First(); sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); // 110 seats currently assigned with a seat minimum of 100 sutProvider.GetDependency().GetAssignedSeatTotalForPlanOrThrow(provider.Id, providerPlan.PlanType).Returns(110); await sutProvider.Sut.AssignSeatsToClientOrganization(provider, organization, seats); // 110 seats - 30 scale down seats = 80 seats, below the 100 seat minimum. await sutProvider.GetDependency().Received(1).AdjustSeats( provider, StaticStore.GetPlan(providerPlan.PlanType), 110, providerPlan.SeatMinimum!.Value); await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( org => org.Id == organization.Id && org.Seats == seats)); // Being below the seat minimum means no purchased seats. await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( pPlan => pPlan.Id == providerPlan.Id && pPlan.PurchasedSeats == 0 && pPlan.AllocatedSeats == 80)); } }