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