diff --git a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs index b16baea52e..1577e77c9e 100644 --- a/src/Billing/Services/Implementations/PaymentSucceededHandler.cs +++ b/src/Billing/Services/Implementations/PaymentSucceededHandler.cs @@ -1,4 +1,5 @@ using Bit.Billing.Constants; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Context; @@ -17,7 +18,6 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler { private readonly ILogger _logger; private readonly IStripeEventService _stripeEventService; - private readonly IOrganizationService _organizationService; private readonly IUserService _userService; private readonly IStripeFacade _stripeFacade; private readonly IProviderRepository _providerRepository; @@ -27,6 +27,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler private readonly IUserRepository _userRepository; private readonly IStripeEventUtilityService _stripeEventUtilityService; private readonly IPushNotificationService _pushNotificationService; + private readonly IOrganizationEnableCommand _organizationEnableCommand; public PaymentSucceededHandler( ILogger logger, @@ -39,8 +40,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler IUserRepository userRepository, IStripeEventUtilityService stripeEventUtilityService, IUserService userService, - IOrganizationService organizationService, - IPushNotificationService pushNotificationService) + IPushNotificationService pushNotificationService, + IOrganizationEnableCommand organizationEnableCommand) { _logger = logger; _stripeEventService = stripeEventService; @@ -52,8 +53,8 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler _userRepository = userRepository; _stripeEventUtilityService = stripeEventUtilityService; _userService = userService; - _organizationService = organizationService; _pushNotificationService = pushNotificationService; + _organizationEnableCommand = organizationEnableCommand; } /// @@ -142,7 +143,7 @@ public class PaymentSucceededHandler : IPaymentSucceededHandler return; } - await _organizationService.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); + await _organizationEnableCommand.EnableAsync(organizationId.Value, subscription.CurrentPeriodEnd); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index ea277a6307..10a1d1a186 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -1,6 +1,7 @@ using Bit.Billing.Constants; using Bit.Billing.Jobs; using Bit.Core; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Platform.Push; using Bit.Core.Repositories; @@ -24,6 +25,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler private readonly IOrganizationRepository _organizationRepository; private readonly ISchedulerFactory _schedulerFactory; private readonly IFeatureService _featureService; + private readonly IOrganizationEnableCommand _organizationEnableCommand; public SubscriptionUpdatedHandler( IStripeEventService stripeEventService, @@ -35,7 +37,8 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler IPushNotificationService pushNotificationService, IOrganizationRepository organizationRepository, ISchedulerFactory schedulerFactory, - IFeatureService featureService) + IFeatureService featureService, + IOrganizationEnableCommand organizationEnableCommand) { _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; @@ -47,6 +50,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _organizationRepository = organizationRepository; _schedulerFactory = schedulerFactory; _featureService = featureService; + _organizationEnableCommand = organizationEnableCommand; } /// @@ -90,7 +94,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } case StripeSubscriptionStatus.Active when organizationId.HasValue: { - await _organizationService.EnableAsync(organizationId.Value); + await _organizationEnableCommand.EnableAsync(organizationId.Value); var organization = await _organizationRepository.GetByIdAsync(organizationId.Value); await _pushNotificationService.PushSyncOrganizationStatusAsync(organization); break; diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs new file mode 100644 index 0000000000..522aa04a60 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IOrganizationEnableCommand.cs @@ -0,0 +1,11 @@ +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; + +public interface IOrganizationEnableCommand +{ + /// + /// Enables an organization that is currently disabled and has a gateway configured. + /// + /// The unique identifier of the organization to enable. + /// When provided, sets the date the organization's subscription will expire. If not provided, no expiration date will be set. + Task EnableAsync(Guid organizationId, DateTime? expirationDate = null); +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs new file mode 100644 index 0000000000..660c792563 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommand.cs @@ -0,0 +1,39 @@ +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public class OrganizationEnableCommand : IOrganizationEnableCommand +{ + private readonly IApplicationCacheService _applicationCacheService; + private readonly IOrganizationRepository _organizationRepository; + + public OrganizationEnableCommand( + IApplicationCacheService applicationCacheService, + IOrganizationRepository organizationRepository) + { + _applicationCacheService = applicationCacheService; + _organizationRepository = organizationRepository; + } + + public async Task EnableAsync(Guid organizationId, DateTime? expirationDate = null) + { + var organization = await _organizationRepository.GetByIdAsync(organizationId); + if (organization is null || organization.Enabled || expirationDate is not null && organization.Gateway is null) + { + return; + } + + organization.Enabled = true; + + if (expirationDate is not null && organization.Gateway is not null) + { + 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 7d73a3c903..683fbe9902 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -28,10 +28,8 @@ public interface IOrganizationService /// Task<(Organization organization, OrganizationUser organizationUser)> SignUpAsync(OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, string privateKey); - Task EnableAsync(Guid organizationId, DateTime? expirationDate); Task DisableAsync(Guid organizationId, DateTime? expirationDate); Task UpdateExpirationDateAsync(Guid organizationId, DateTime? expirationDate); - Task EnableAsync(Guid organizationId); Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated); Task UpdateTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 6f4aba4882..284c11cc78 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -686,18 +686,6 @@ public class OrganizationService : IOrganizationService } } - public async Task EnableAsync(Guid organizationId, DateTime? expirationDate) - { - var org = await GetOrgById(organizationId); - if (org != null && !org.Enabled && org.Gateway.HasValue) - { - org.Enabled = true; - org.ExpirationDate = expirationDate; - org.RevisionDate = DateTime.UtcNow; - await ReplaceAndUpdateCacheAsync(org); - } - } - public async Task DisableAsync(Guid organizationId, DateTime? expirationDate) { var org = await GetOrgById(organizationId); @@ -723,16 +711,6 @@ public class OrganizationService : IOrganizationService } } - public async Task EnableAsync(Guid organizationId) - { - var org = await GetOrgById(organizationId); - if (org != null && !org.Enabled) - { - org.Enabled = true; - await ReplaceAndUpdateCacheAsync(org); - } - } - public async Task UpdateAsync(Organization organization, bool updateBilling = false, EventType eventType = EventType.Organization_Updated) { if (organization.Id == default(Guid)) diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 9d2e6e51e6..7db514887c 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -54,6 +54,7 @@ public static class OrganizationServiceCollectionExtensions services.AddOrganizationDomainCommandsQueries(); services.AddOrganizationSignUpCommands(); services.AddOrganizationDeleteCommands(); + services.AddOrganizationEnableCommands(); services.AddOrganizationAuthCommands(); services.AddOrganizationUserCommands(); services.AddOrganizationUserCommandsQueries(); @@ -69,6 +70,9 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); } + private static void AddOrganizationEnableCommands(this IServiceCollection services) => + services.AddScoped(); + private static void AddOrganizationConnectionCommands(this IServiceCollection services) { services.AddScoped(); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs new file mode 100644 index 0000000000..6289c3b8e3 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationEnableCommandTests.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +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; + +[SutProviderCustomize] +public class OrganizationEnableCommandTests +{ + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationDoesNotExist_DoesNothing( + Guid organizationId, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(organizationId) + .Returns((Organization)null); + + await sutProvider.Sut.EnableAsync(organizationId); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationAlreadyEnabled_DoesNothing( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = true; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WhenOrganizationDisabled_EnablesAndSaves( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + Assert.True(organization.Enabled); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithExpiration_WhenOrganizationHasNoGateway_DoesNothing( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + organization.Gateway = null; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id, expirationDate); + + await sutProvider.GetDependency() + .DidNotReceive() + .ReplaceAsync(Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceive() + .UpsertOrganizationAbilityAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithExpiration_WhenValid_EnablesAndSetsExpiration( + Organization organization, + DateTime expirationDate, + SutProvider sutProvider) + { + organization.Enabled = false; + organization.Gateway = GatewayType.Stripe; + organization.RevisionDate = DateTime.UtcNow.AddDays(-1); + var originalRevisionDate = organization.RevisionDate; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id, expirationDate); + + Assert.True(organization.Enabled); + Assert.Equal(expirationDate, organization.ExpirationDate); + Assert.True(organization.RevisionDate > originalRevisionDate); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } + + [Theory, BitAutoData] + public async Task EnableAsync_WithoutExpiration_DoesNotUpdateRevisionDate( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + var originalRevisionDate = organization.RevisionDate; + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + await sutProvider.Sut.EnableAsync(organization.Id); + + Assert.True(organization.Enabled); + Assert.Equal(originalRevisionDate, organization.RevisionDate); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(organization); + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(organization); + } +}