diff --git a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs index 26a1c30c14..8155928453 100644 --- a/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionDeletedHandler.cs @@ -1,4 +1,5 @@ using Bit.Billing.Constants; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.Services; using Event = Stripe.Event; namespace Bit.Billing.Services.Implementations; @@ -6,20 +7,20 @@ namespace Bit.Billing.Services.Implementations; public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler { private readonly IStripeEventService _stripeEventService; - private readonly IOrganizationService _organizationService; private readonly IUserService _userService; private readonly IStripeEventUtilityService _stripeEventUtilityService; + private readonly IOrganizationDisableCommand _organizationDisableCommand; public SubscriptionDeletedHandler( IStripeEventService stripeEventService, - IOrganizationService organizationService, IUserService userService, - IStripeEventUtilityService stripeEventUtilityService) + IStripeEventUtilityService stripeEventUtilityService, + IOrganizationDisableCommand organizationDisableCommand) { _stripeEventService = stripeEventService; - _organizationService = organizationService; _userService = userService; _stripeEventUtilityService = stripeEventUtilityService; + _organizationDisableCommand = organizationDisableCommand; } /// @@ -44,7 +45,7 @@ public class SubscriptionDeletedHandler : ISubscriptionDeletedHandler subscription.CancellationDetails.Comment != providerMigrationCancellationComment && !subscription.CancellationDetails.Comment.Contains(addedToProviderCancellationComment)) { - await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); } else if (userId.HasValue) { diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index 4e142f8cae..35a16ae74f 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -26,6 +26,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly ISchedulerFactory _schedulerFactory; private readonly IFeatureService _featureService; private readonly IOrganizationEnableCommand _organizationEnableCommand; + private readonly IOrganizationDisableCommand _organizationDisableCommand; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -38,7 +39,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, IFeatureService featureService, - IOrganizationEnableCommand organizationEnableCommand) + IOrganizationEnableCommand organizationEnableCommand, + IOrganizationDisableCommand organizationDisableCommand) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -51,6 +53,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _schedulerFactory = schedulerFactory; _featureService = featureService; _organizationEnableCommand = organizationEnableCommand; + _organizationDisableCommand = organizationDisableCommand; } /// @@ -67,7 +70,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler case StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired when organizationId.HasValue: { - await _organizationService.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationDisableCommand.DisableAsync(organizationId.Value, subscription.CurrentPeriodEnd); if (subscription.Status == StripeSubscriptionStatus.Unpaid && subscription.LatestInvoice is { BillingReason: "subscription_cycle" or "subscription_create" }) { diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs new file mode 100644 index 0000000000..d15e9537e6 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationDisableCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +/// +/// Command interface for disabling organizations. +/// +public interface IOrganizationDisableCommand +{ + /// + /// Disables an organization with an optional expiration date. + /// + /// The unique identifier of the organization to disable. + /// Optional date when the disable status should expire. + Task DisableAsync(Guid organizationId, DateTime? expirationDate); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs new file mode 100644 index 0000000000..63f80032b8 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommand.cs @@ -0,0 +1,33 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationDisableCommand : IOrganizationDisableCommand +{ + private readonly IOrganizationRepository _organizationRepository; + private readonly IApplicationCacheService _applicationCacheService; + + public OrganizationDisableCommand( + IOrganizationRepository organizationRepository, + IApplicationCacheService applicationCacheService) + { + _organizationRepository = organizationRepository; + _applicationCacheService = applicationCacheService; + } + + public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization is { Enabled: true }) + { + organization.Enabled = false; + organization.ExpirationDate = expirationDate; + organization.RevisionDate = DateTime.UtcNow; + + await _organizationRepository.ReplaceAsync(organization); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + } + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 683fbe9902..dacb2ab162 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -28,7 +28,6 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); - Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 284c11cc78..14cf89a246 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -686,20 +686,6 @@ public class OrganizationService : IOrganizationService } } - public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) - { - var org = await GetOrgById(organizationId); - if (org != null && org.Enabled) - { - org.Enabled = false; - org.ExpirationDate = expirationDate; - org.RevisionDate = DateTime.UtcNow; - await ReplaceAndUpdateCacheAsync(org); - - // TODO: send email to owners? - } - } - public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 7db514887c..232e04fbd0 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -55,6 +55,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); services.AddOrganizationEnableCommands(); + services.AddOrganizationDisableCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); @@ -73,6 +74,9 @@ public static class OrganizationServiceCollectionExtensions private static void AddOrganizationEnableCommands(this IServiceCollection services) => services.AddScoped(); + private static void AddOrganizationDisableCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs new file mode 100644 index 0000000000..9e77a56b93 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationDisableCommandTests.cs @@ -0,0 +1,79 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +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; + +[SutProviderCustomize] +public class OrganizationDisableCommandTests +{ + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationEnabled_DisablesSuccessfully( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.DisableAsync(organization.Id, expirationDate); + + Assert.False(organization.Enabled); + Assert.Equal(expirationDate, organization.ExpirationDate); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationNotFound_DoesNothing( + Guid organizationId, + DateTime expirationDate, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns((Organization)null); + + await sutProvider.Sut.DisableAsync(organizationId, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task DisableAsync_WhenOrganizationAlreadyDisabled_DoesNothing( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.DisableAsync(organization.Id, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } +}