1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-23287] Enable Provider When Subscription Is Paid (#6113)

* test : add tests for provider update

* feat: add provider update logic and dependencies

* fix: remove duplicate dependencies

* refactor: updated switch logic for helper method

* test: add feature flag to tests

* feat: add feature flag for changes
This commit is contained in:
Stephon Brown
2025-07-24 11:46:16 -04:00
committed by GitHub
parent 2cf7208eb3
commit 76d1a2e875
2 changed files with 407 additions and 1 deletions

View File

@@ -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
}
}
/// <summary>
/// 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.
/// </summary>
/// <param name="parsedEvent">The event containing the previous subscription status</param>
/// <param name="subscription">The current subscription status</param>
/// <returns>A boolean that represents whether the event status has changed from a non-active status to an active status</returns>
private static bool IsProviderSubscriptionNowActive(Event parsedEvent, Subscription subscription)
{
if (parsedEvent.Data.PreviousAttributes == null)
{
return false;
}
var previousSubscription = parsedEvent
.Data
.PreviousAttributes
.ToObject<Subscription>() as Subscription;
return previousSubscription?.Status switch
{
StripeSubscriptionStatus.IncompleteExpired
or StripeSubscriptionStatus.Paused
or StripeSubscriptionStatus.Incomplete
or StripeSubscriptionStatus.Unpaid
when subscription.Status == StripeSubscriptionStatus.Active => true,
_ => false
};
}
/// <summary>
/// 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.

View File

@@ -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<IStripeFacade>();
_organizationSponsorshipRenewCommand = Substitute.For<IOrganizationSponsorshipRenewCommand>();
_userService = Substitute.For<IUserService>();
_providerService = Substitute.For<IProviderService>();
_pushNotificationService = Substitute.For<IPushNotificationService>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
_providerRepository = Substitute.For<IProviderRepository>();
_schedulerFactory = Substitute.For<ISchedulerFactory>();
_organizationEnableCommand = Substitute.For<IOrganizationEnableCommand>();
_organizationDisableCommand = Substitute.For<IOrganizationDisableCommand>();
@@ -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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_stripeFacade
.UpdateSubscription(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>())
.Returns(newSubscription);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.Received(1)
.UpdateSubscription(newSubscription.Id,
Arg.Is<SubscriptionUpdateOptions>(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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository.Received(1).GetByIdAsync(providerId);
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(Arg.Any<Guid>());
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceiveWithAnyArgs()
.UpdateSubscription(Arg.Any<string>());
_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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.ReturnsNull();
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(providerId);
await _providerService
.DidNotReceive()
.UpdateAsync(Arg.Any<Provider>());
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_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<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
.Returns(newSubscription);
_stripeEventUtilityService
.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, null, providerId));
_providerRepository
.GetByIdAsync(Arg.Any<Guid>())
.Returns(provider);
_featureService.IsEnabled(FeatureFlagKeys.PM21821_ProviderPortalTakeover)
.Returns(true);
// Act
await _sut.HandleAsync(parsedEvent);
// Assert
await _stripeEventService
.Received(1)
.GetSubscription(parsedEvent, true, Arg.Any<List<string>>());
_stripeEventUtilityService
.Received(1)
.GetIdsFromMetadata(newSubscription.Metadata);
await _providerRepository
.Received(1)
.GetByIdAsync(Arg.Any<Guid>());
await _providerService
.Received(1)
.UpdateAsync(Arg.Is<Provider>(p => p.Id == providerId && p.Enabled == true));
await _stripeFacade
.DidNotReceive()
.UpdateSubscription(Arg.Any<string>());
_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<string, string> { { "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<object[]> GetNonActiveSubscriptions()
{
return new List<object[]>
{
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 }, },
};
}
}