mirror of
https://github.com/bitwarden/server
synced 2025-12-16 00:03:54 +00:00
Fix the Bug
This commit is contained in:
@@ -111,7 +111,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
break;
|
||||
}
|
||||
|
||||
if (subscription.Status is StripeSubscriptionStatus.Unpaid &&
|
||||
if ((subscription.Status is StripeSubscriptionStatus.Unpaid or StripeSubscriptionStatus.IncompleteExpired) &&
|
||||
subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore))
|
||||
{
|
||||
await CancelSubscription(subscription.Id);
|
||||
@@ -120,6 +120,20 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
||||
|
||||
await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);
|
||||
|
||||
break;
|
||||
}
|
||||
case StripeSubscriptionStatus.Incomplete when userId.HasValue:
|
||||
{
|
||||
// Handle Incomplete subscriptions for Premium users that have open invoices from failed payments
|
||||
// This prevents duplicate subscriptions when users retry the subscription flow
|
||||
if (subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore) &&
|
||||
subscription.LatestInvoice is { Status: StripeInvoiceStatus.Open })
|
||||
{
|
||||
await CancelSubscription(subscription.Id);
|
||||
await VoidOpenInvoices(subscription.Id);
|
||||
await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case StripeSubscriptionStatus.Active when organizationId.HasValue:
|
||||
|
||||
@@ -552,6 +552,55 @@ public class SubscriptionUpdatedHandlerTests
|
||||
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_IncompleteExpiredUserSubscription_DisablesPremiumAndCancelsSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var subscriptionId = "sub_123";
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
Status = StripeSubscriptionStatus.IncompleteExpired,
|
||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = currentPeriodEnd,
|
||||
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var parsedEvent = new Event { Data = new EventData() };
|
||||
|
||||
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
|
||||
.Returns(subscription);
|
||||
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>())
|
||||
.Returns(new StripeList<Invoice> { Data = new List<Invoice>() });
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(1)
|
||||
.DisablePremiumAsync(userId, currentPeriodEnd);
|
||||
await _stripeFacade.Received(1)
|
||||
.CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||
await _stripeFacade.Received(1)
|
||||
.ListInvoices(Arg.Is<InvoiceListOptions>(o =>
|
||||
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_ActiveOrganizationSubscription_EnablesOrganizationAndUpdatesExpiration()
|
||||
{
|
||||
@@ -1098,6 +1147,114 @@ public class SubscriptionUpdatedHandlerTests
|
||||
return (providerId, newSubscription, provider, parsedEvent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_IncompleteUserSubscriptionWithOpenInvoice_CancelsSubscriptionAndDisablesPremium()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var subscriptionId = "sub_123";
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
var openInvoice = new Invoice
|
||||
{
|
||||
Id = "inv_123",
|
||||
Status = StripeInvoiceStatus.Open
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
Status = StripeSubscriptionStatus.Incomplete,
|
||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
|
||||
LatestInvoice = openInvoice,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = currentPeriodEnd,
|
||||
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var parsedEvent = new Event { Data = new EventData() };
|
||||
|
||||
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
|
||||
.Returns(subscription);
|
||||
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
_stripeFacade.ListInvoices(Arg.Any<InvoiceListOptions>())
|
||||
.Returns(new StripeList<Invoice> { Data = new List<Invoice> { openInvoice } });
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userService.Received(1)
|
||||
.DisablePremiumAsync(userId, currentPeriodEnd);
|
||||
await _stripeFacade.Received(1)
|
||||
.CancelSubscription(subscriptionId, Arg.Any<SubscriptionCancelOptions>());
|
||||
await _stripeFacade.Received(1)
|
||||
.ListInvoices(Arg.Is<InvoiceListOptions>(o =>
|
||||
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
|
||||
await _stripeFacade.Received(1)
|
||||
.VoidInvoice(openInvoice.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_IncompleteUserSubscriptionWithoutOpenInvoice_DoesNotCancelSubscription()
|
||||
{
|
||||
// Arrange
|
||||
var userId = Guid.NewGuid();
|
||||
var subscriptionId = "sub_123";
|
||||
var currentPeriodEnd = DateTime.UtcNow.AddDays(30);
|
||||
var paidInvoice = new Invoice
|
||||
{
|
||||
Id = "inv_123",
|
||||
Status = StripeInvoiceStatus.Paid
|
||||
};
|
||||
var subscription = new Subscription
|
||||
{
|
||||
Id = subscriptionId,
|
||||
Status = StripeSubscriptionStatus.Incomplete,
|
||||
Metadata = new Dictionary<string, string> { { "userId", userId.ToString() } },
|
||||
LatestInvoice = paidInvoice,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
CurrentPeriodEnd = currentPeriodEnd,
|
||||
Price = new Price { Id = IStripeEventUtilityService.PremiumPlanId }
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
var parsedEvent = new Event { Data = new EventData() };
|
||||
|
||||
_stripeEventService.GetSubscription(Arg.Any<Event>(), Arg.Any<bool>(), Arg.Any<List<string>>())
|
||||
.Returns(subscription);
|
||||
|
||||
_stripeEventUtilityService.GetIdsFromMetadata(Arg.Any<Dictionary<string, string>>())
|
||||
.Returns(Tuple.Create<Guid?, Guid?, Guid?>(null, userId, null));
|
||||
|
||||
// Act
|
||||
await _sut.HandleAsync(parsedEvent);
|
||||
|
||||
// Assert
|
||||
await _userService.DidNotReceive()
|
||||
.DisablePremiumAsync(Arg.Any<Guid>(), Arg.Any<DateTime?>());
|
||||
await _stripeFacade.DidNotReceive()
|
||||
.CancelSubscription(Arg.Any<string>(), Arg.Any<SubscriptionCancelOptions>());
|
||||
await _stripeFacade.DidNotReceive()
|
||||
.ListInvoices(Arg.Any<InvoiceListOptions>());
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> GetNonActiveSubscriptions()
|
||||
{
|
||||
return new List<object[]>
|
||||
|
||||
Reference in New Issue
Block a user