diff --git a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs index bbc17aa3b2..702d9aaf3d 100644 --- a/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs +++ b/src/Billing/Services/Implementations/SubscriptionUpdatedHandler.cs @@ -56,11 +56,13 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler _stripeEventService = stripeEventService; _stripeEventUtilityService = stripeEventUtilityService; _organizationService = organizationService; + _providerService = providerService; _stripeFacade = stripeFacade; _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand; _userService = userService; _pushNotificationService = pushNotificationService; _organizationRepository = organizationRepository; + _providerRepository = providerRepository; _schedulerFactory = schedulerFactory; _organizationEnableCommand = organizationEnableCommand; _organizationDisableCommand = organizationDisableCommand; @@ -126,13 +128,34 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } break; } + case StripeSubscriptionStatus.Active when providerId.HasValue: + { + var providerPortalTakeover = _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + if (!providerPortalTakeover) + { + break; + } + var provider = await _providerRepository.GetByIdAsync(providerId.Value); + if (provider != null) + { + provider.Enabled = true; + await _providerService.UpdateAsync(provider); + + if (IsProviderSubscriptionNowActive(parsedEvent, subscription)) + { + // Update the CancelAtPeriodEnd subscription option to prevent the now active provider subscription from being cancelled + var subscriptionUpdateOptions = new SubscriptionUpdateOptions { CancelAtPeriodEnd = false }; + await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionUpdateOptions); + } + } + break; + } case StripeSubscriptionStatus.Active: { if (userId.HasValue) { await _userService.EnablePremiumAsync(userId.Value, subscription.CurrentPeriodEnd); } - break; } } @@ -170,6 +193,36 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler } } + /// + /// Checks if the provider subscription status has changed from a non-active to an active status type + /// If the previous status is already active(active,past-due,trialing),canceled,or null, then this will return false. + /// + /// The event containing the previous subscription status + /// The current subscription status + /// A boolean that represents whether the event status has changed from a non-active status to an active status + private static bool IsProviderSubscriptionNowActive(Event parsedEvent, Subscription subscription) + { + if (parsedEvent.Data.PreviousAttributes == null) + { + return false; + } + + var previousSubscription = parsedEvent + .Data + .PreviousAttributes + .ToObject() as Subscription; + + return previousSubscription?.Status switch + { + StripeSubscriptionStatus.IncompleteExpired + or StripeSubscriptionStatus.Paused + or StripeSubscriptionStatus.Incomplete + or StripeSubscriptionStatus.Unpaid + when subscription.Status == StripeSubscriptionStatus.Active => true, + _ => false + }; + } + /// /// Removes the Password Manager coupon if the organization is removing the Secrets Manager trial. /// Only applies to organizations that have a subscription from the Secrets Manager trial. diff --git a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs index ce4ee608cc..f230b87dea 100644 --- a/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs +++ b/test/Billing.Test/Services/SubscriptionUpdatedHandlerTests.cs @@ -17,6 +17,7 @@ using Bit.Core.Services; using Microsoft.Extensions.Logging; using Newtonsoft.Json.Linq; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Quartz; using Stripe; using Stripe.TestHelpers; @@ -54,8 +55,10 @@ public class SubscriptionUpdatedHandlerTests _stripeFacade = Substitute.For(); _organizationSponsorshipRenewCommand = Substitute.For(); _userService = Substitute.For(); + _providerService = Substitute.For(); _pushNotificationService = Substitute.For(); _organizationRepository = Substitute.For(); + _providerRepository = Substitute.For(); _schedulerFactory = Substitute.For(); _organizationEnableCommand = Substitute.For(); _organizationDisableCommand = Substitute.For(); @@ -663,4 +666,354 @@ public class SubscriptionUpdatedHandlerTests await _stripeFacade.Received(1).DeleteCustomerDiscount(subscription.CustomerId); await _stripeFacade.Received(1).DeleteSubscriptionDiscount(subscription.Id); } + + [Theory] + [MemberData(nameof(GetNonActiveSubscriptions))] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasNonActive_EnableProviderAndUpdateSubscription( + Subscription previousSubscription) + { + // Arrange + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _stripeFacade + .UpdateSubscription(Arg.Any(), Arg.Any()) + .Returns(newSubscription); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .Received(1) + .UpdateSubscription(newSubscription.Id, + Arg.Is(options => options.CancelAtPeriodEnd == false)); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasCanceled_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Canceled }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasAlreadyActive_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Active }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasTrailing_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Trialing }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository.Received(1).GetByIdAsync(providerId); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task + HandleAsync_ActiveProviderSubscriptionEvent_AndPreviousSubscriptionStatusWasPastDue_EnableProvider() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.PastDue }; + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(Arg.Any()); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceiveWithAnyArgs() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task HandleAsync_ActiveProviderSubscriptionEvent_AndProviderDoesNotExist_NoChanges() + { + // Arrange + var previousSubscription = new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid }; + var (providerId, newSubscription, _, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(previousSubscription); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .ReturnsNull(); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(providerId); + await _providerService + .DidNotReceive() + .UpdateAsync(Arg.Any()); + await _stripeFacade + .DidNotReceive() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + [Fact] + public async Task HandleAsync_ActiveProviderSubscriptionEvent_WithNoPreviousAttributes_EnableProvider() + { + // Arrange + var (providerId, newSubscription, provider, parsedEvent) = + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(null); + + _stripeEventService + .GetSubscription(Arg.Any(), Arg.Any(), Arg.Any>()) + .Returns(newSubscription); + _stripeEventUtilityService + .GetIdsFromMetadata(Arg.Any>()) + .Returns(Tuple.Create(null, null, providerId)); + _providerRepository + .GetByIdAsync(Arg.Any()) + .Returns(provider); + _featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover) + .Returns(true); + + // Act + await _sut.HandleAsync(parsedEvent); + + // Assert + await _stripeEventService + .Received(1) + .GetSubscription(parsedEvent, true, Arg.Any>()); + _stripeEventUtilityService + .Received(1) + .GetIdsFromMetadata(newSubscription.Metadata); + await _providerRepository + .Received(1) + .GetByIdAsync(Arg.Any()); + await _providerService + .Received(1) + .UpdateAsync(Arg.Is(p => p.Id == providerId && p.Enabled == true)); + await _stripeFacade + .DidNotReceive() + .UpdateSubscription(Arg.Any()); + _featureService + .Received(1) + .IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover); + } + + private static (Guid providerId, Subscription newSubscription, Provider provider, Event parsedEvent) + CreateProviderTestInputsForUpdatedActiveSubscriptionStatus(Subscription? previousSubscription) + { + var providerId = Guid.NewGuid(); + var newSubscription = new Subscription + { + Id = previousSubscription?.Id ?? "sub_123", + Status = StripeSubscriptionStatus.Active, + Metadata = new Dictionary { { "providerId", providerId.ToString() } }, + }; + + var provider = new Provider { Id = providerId, Enabled = false }; + var parsedEvent = new Event + { + Data = new EventData + { + Object = newSubscription, + PreviousAttributes = + previousSubscription == null ? null : JObject.FromObject(previousSubscription) + } + }; + return (providerId, newSubscription, provider, parsedEvent); + } + + public static IEnumerable GetNonActiveSubscriptions() + { + return new List + { + new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Unpaid }, }, + new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Incomplete }, }, + new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.IncompleteExpired }, }, + new object[] { new Subscription { Id = "sub_123", Status = StripeSubscriptionStatus.Paused }, }, + }; + } }