diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index cb64ea2d4..5d89be0c4 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -3,10 +3,12 @@ using Bit.Admin.AdminConsole.Models; using Bit.Admin.Enums; using Bit.Admin.Services; using Bit.Admin.Utilities; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Extensions; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -53,6 +55,8 @@ public class OrganizationsController : Controller private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand; private readonly IRemovePaymentMethodCommand _removePaymentMethodCommand; + private readonly IFeatureService _featureService; + private readonly IScaleSeatsCommand _scaleSeatsCommand; public OrganizationsController( IOrganizationService organizationService, @@ -78,7 +82,9 @@ public class OrganizationsController : Controller IServiceAccountRepository serviceAccountRepository, IProviderOrganizationRepository providerOrganizationRepository, IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand, - IRemovePaymentMethodCommand removePaymentMethodCommand) + IRemovePaymentMethodCommand removePaymentMethodCommand, + IFeatureService featureService, + IScaleSeatsCommand scaleSeatsCommand) { _organizationService = organizationService; _organizationRepository = organizationRepository; @@ -104,6 +110,8 @@ public class OrganizationsController : Controller _providerOrganizationRepository = providerOrganizationRepository; _removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand; _removePaymentMethodCommand = removePaymentMethodCommand; + _featureService = featureService; + _scaleSeatsCommand = scaleSeatsCommand; } [RequirePermission(Permission.Org_List_View)] @@ -234,12 +242,30 @@ public class OrganizationsController : Controller public async Task Delete(Guid id) { var organization = await _organizationRepository.GetByIdAsync(id); - if (organization != null) + + if (organization == null) { - await _organizationRepository.DeleteAsync(organization); - await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + return RedirectToAction("Index"); } + var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + if (consolidatedBillingEnabled && organization.IsValidClient()) + { + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + + if (provider.IsBillable()) + { + await _scaleSeatsCommand.ScalePasswordManagerSeats( + provider, + organization.PlanType, + -organization.Seats ?? 0); + } + } + + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + return RedirectToAction("Index"); } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index 7231f29c4..a05fe050e 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -20,6 +20,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Billing.Commands; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Queries; using Bit.Core.Context; @@ -69,6 +70,8 @@ public class OrganizationsController : Controller private readonly ISubscriberQueries _subscriberQueries; private readonly IReferenceEventService _referenceEventService; private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; + private readonly IProviderRepository _providerRepository; + private readonly IScaleSeatsCommand _scaleSeatsCommand; public OrganizationsController( IOrganizationRepository organizationRepository, @@ -95,7 +98,9 @@ public class OrganizationsController : Controller ICancelSubscriptionCommand cancelSubscriptionCommand, ISubscriberQueries subscriberQueries, IReferenceEventService referenceEventService, - IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand) + IOrganizationEnableCollectionEnhancementsCommand organizationEnableCollectionEnhancementsCommand, + IProviderRepository providerRepository, + IScaleSeatsCommand scaleSeatsCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -122,6 +127,8 @@ public class OrganizationsController : Controller _subscriberQueries = subscriberQueries; _referenceEventService = referenceEventService; _organizationEnableCollectionEnhancementsCommand = organizationEnableCollectionEnhancementsCommand; + _providerRepository = providerRepository; + _scaleSeatsCommand = scaleSeatsCommand; } [HttpGet("{id}")] @@ -560,10 +567,23 @@ public class OrganizationsController : Controller await Task.Delay(2000); throw new BadRequestException(string.Empty, "User verification failed."); } - else + + var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling); + + if (consolidatedBillingEnabled && organization.IsValidClient()) { - await _organizationService.DeleteAsync(organization); + var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id); + + if (provider.IsBillable()) + { + await _scaleSeatsCommand.ScalePasswordManagerSeats( + provider, + organization.PlanType, + -organization.Seats ?? 0); + } } + + await _organizationService.DeleteAsync(organization); } [HttpPost("{id}/import")] diff --git a/src/Core/Billing/Extensions/BillingExtensions.cs b/src/Core/Billing/Extensions/BillingExtensions.cs index c7abeb81e..9b1bb9e92 100644 --- a/src/Core/Billing/Extensions/BillingExtensions.cs +++ b/src/Core/Billing/Extensions/BillingExtensions.cs @@ -1,9 +1,27 @@ -using Bit.Core.Enums; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.Enums; namespace Bit.Core.Billing.Extensions; public static class BillingExtensions { + public static bool IsBillable(this Provider provider) => + provider is + { + Type: ProviderType.Msp, + Status: ProviderStatusType.Billable + }; + + public static bool IsValidClient(this Organization organization) + => organization is + { + Seats: not null, + Status: OrganizationStatusType.Managed, + PlanType: PlanType.TeamsMonthly or PlanType.EnterpriseMonthly + }; + public static bool SupportsConsolidatedBilling(this PlanType planType) => planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly; } diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 9d3c7ebfe..831212f1b 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -2,8 +2,11 @@ using AutoFixture.Xunit2; using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.Auth.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; +using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationCollectionEnhancements.Interfaces; using Bit.Core.AdminConsole.Repositories; @@ -25,6 +28,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Tools.Services; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; using NSubstitute; using NSubstitute.ReturnsExtensions; using Xunit; @@ -59,6 +63,8 @@ public class OrganizationsControllerTests : IDisposable private readonly ISubscriberQueries _subscriberQueries; private readonly IReferenceEventService _referenceEventService; private readonly IOrganizationEnableCollectionEnhancementsCommand _organizationEnableCollectionEnhancementsCommand; + private readonly IProviderRepository _providerRepository; + private readonly IScaleSeatsCommand _scaleSeatsCommand; private readonly OrganizationsController _sut; @@ -89,6 +95,8 @@ public class OrganizationsControllerTests : IDisposable _subscriberQueries = Substitute.For(); _referenceEventService = Substitute.For(); _organizationEnableCollectionEnhancementsCommand = Substitute.For(); + _providerRepository = Substitute.For(); + _scaleSeatsCommand = Substitute.For(); _sut = new OrganizationsController( _organizationRepository, @@ -115,7 +123,9 @@ public class OrganizationsControllerTests : IDisposable _cancelSubscriptionCommand, _subscriberQueries, _referenceEventService, - _organizationEnableCollectionEnhancementsCommand); + _organizationEnableCollectionEnhancementsCommand, + _providerRepository, + _scaleSeatsCommand); } public void Dispose() @@ -414,4 +424,39 @@ public class OrganizationsControllerTests : IDisposable await _organizationEnableCollectionEnhancementsCommand.DidNotReceiveWithAnyArgs().EnableCollectionEnhancements(Arg.Any()); await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncOrganizationsAsync(Arg.Any()); } + + [Theory, AutoData] + public async Task Delete_OrganizationIsConsolidatedBillingClient_ScalesProvidersSeats( + Provider provider, + Organization organization, + User user, + Guid organizationId, + SecretVerificationRequestModel requestModel) + { + organization.Status = OrganizationStatusType.Managed; + organization.PlanType = PlanType.TeamsMonthly; + organization.Seats = 10; + + provider.Type = ProviderType.Msp; + provider.Status = ProviderStatusType.Billable; + + _currentContext.OrganizationOwner(organizationId).Returns(true); + + _organizationRepository.GetByIdAsync(organizationId).Returns(organization); + + _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); + + _userService.VerifySecretAsync(user, requestModel.Secret).Returns(true); + + _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling).Returns(true); + + _providerRepository.GetByOrganizationIdAsync(organization.Id).Returns(provider); + + await _sut.Delete(organizationId.ToString(), requestModel); + + await _scaleSeatsCommand.Received(1) + .ScalePasswordManagerSeats(provider, organization.PlanType, -organization.Seats.Value); + + await _organizationService.Received(1).DeleteAsync(organization); + } }