mirror of
https://github.com/bitwarden/server
synced 2025-12-22 19:23:45 +00:00
Fix the Bug
This commit is contained in:
@@ -111,7 +111,7 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
break;
|
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))
|
subscription.Items.Any(i => i.Price.Id is IStripeEventUtilityService.PremiumPlanId or IStripeEventUtilityService.PremiumPlanIdAppStore))
|
||||||
{
|
{
|
||||||
await CancelSubscription(subscription.Id);
|
await CancelSubscription(subscription.Id);
|
||||||
@@ -120,6 +120,20 @@ public class SubscriptionUpdatedHandler : ISubscriptionUpdatedHandler
|
|||||||
|
|
||||||
await _userService.DisablePremiumAsync(userId.Value, currentPeriodEnd);
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case StripeSubscriptionStatus.Active when organizationId.HasValue:
|
case StripeSubscriptionStatus.Active when organizationId.HasValue:
|
||||||
|
|||||||
@@ -552,6 +552,55 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
o.Status == StripeInvoiceStatus.Open && o.Subscription == subscriptionId));
|
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]
|
[Fact]
|
||||||
public async Task HandleAsync_ActiveOrganizationSubscription_EnablesOrganizationAndUpdatesExpiration()
|
public async Task HandleAsync_ActiveOrganizationSubscription_EnablesOrganizationAndUpdatesExpiration()
|
||||||
{
|
{
|
||||||
@@ -1098,6 +1147,114 @@ public class SubscriptionUpdatedHandlerTests
|
|||||||
return (providerId, newSubscription, provider, parsedEvent);
|
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()
|
public static IEnumerable<object[]> GetNonActiveSubscriptions()
|
||||||
{
|
{
|
||||||
return new List<object[]>
|
return new List<object[]>
|
||||||
|
|||||||
Reference in New Issue
Block a user