mirror of
https://github.com/bitwarden/server
synced 2026-02-25 17:03:22 +00:00
[PM-30908]Correct Premium subscription status handling (#6877)
* Implement the correct changes * failing test has been removed * Add unit testing and logs * Resolve the pr comment on missed requirements * fix the lint error * resolve the build lint * Fix the failing test * Fix the failing test * Add the IncompleteExpired status * resolve the lint error * Fix the build lint error * Implement the IncompleteExpired flow
This commit is contained in:
@@ -3,6 +3,7 @@ using Bit.Core.Billing.Commands;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Subscriptions.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@@ -143,6 +144,24 @@ public class UpdatePaymentMethodCommand(
|
||||
await stripeAdapter.UpdateCustomerAsync(customer.Id, new CustomerUpdateOptions { Metadata = metadata });
|
||||
}
|
||||
|
||||
// If the subscriber has an incomplete subscription, pay the invoice with the new PayPal payment method
|
||||
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
|
||||
{
|
||||
var subscription = await stripeAdapter.GetSubscriptionAsync(subscriber.GatewaySubscriptionId);
|
||||
|
||||
if (subscription.Status == StripeConstants.SubscriptionStatus.Incomplete)
|
||||
{
|
||||
var invoice = await stripeAdapter.UpdateInvoiceAsync(subscription.LatestInvoiceId,
|
||||
new InvoiceUpdateOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
Expand = ["customer"]
|
||||
});
|
||||
|
||||
await braintreeService.PayInvoice(new UserId(subscriber.Id), invoice);
|
||||
}
|
||||
}
|
||||
|
||||
var payPalAccount = braintreeCustomer.DefaultPaymentMethod as PayPalAccount;
|
||||
|
||||
return MaskedPaymentMethod.From(payPalAccount!);
|
||||
|
||||
@@ -72,7 +72,13 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb) => HandleAsync<None>(async () =>
|
||||
{
|
||||
if (user.Premium)
|
||||
// A "terminal" subscription is one that has ended and cannot be renewed/reactivated.
|
||||
// These are: 'canceled' (user canceled) and 'incomplete_expired' (payment failed and time expired).
|
||||
// We allow users with terminal subscriptions to create a new subscription even if user.Premium is still true,
|
||||
// enabling the resubscribe workflow without requiring Premium status to be cleared first.
|
||||
var hasTerminalSubscription = await HasTerminalSubscriptionAsync(user);
|
||||
|
||||
if (user.Premium && !hasTerminalSubscription)
|
||||
{
|
||||
return new BadRequest("Already a premium user.");
|
||||
}
|
||||
@@ -98,8 +104,11 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
* purchased account credit but chose to use a tokenizable payment method to pay for the subscription. In this case,
|
||||
* we need to add the payment method to their customer first. If the incoming payment method is account credit,
|
||||
* we can just go straight to fetching the customer since there's no payment method to apply.
|
||||
*
|
||||
* Additionally, if this is a resubscribe scenario with a tokenized payment method, we should update the payment method
|
||||
* to ensure the new payment method is used instead of the old one.
|
||||
*/
|
||||
else if (paymentMethod.IsTokenized && !await hasPaymentMethodQuery.Run(user))
|
||||
else if (paymentMethod.IsTokenized && (!await hasPaymentMethodQuery.Run(user) || hasTerminalSubscription))
|
||||
{
|
||||
await updatePaymentMethodCommand.Run(user, paymentMethod.AsTokenized, billingAddress);
|
||||
customer = await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });
|
||||
@@ -122,7 +131,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
case { Type: TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == SubscriptionStatus.Incomplete:
|
||||
case { Type: not TokenizablePaymentMethodType.PayPal }
|
||||
when subscription.Status == SubscriptionStatus.Active:
|
||||
when subscription.Status is SubscriptionStatus.Active or SubscriptionStatus.Incomplete:
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
|
||||
@@ -369,4 +378,28 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private async Task<bool> HasTerminalSubscriptionAsync(User user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.GatewaySubscriptionId))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var existingSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
|
||||
return existingSubscription.Status is
|
||||
SubscriptionStatus.Canceled or
|
||||
SubscriptionStatus.IncompleteExpired;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Subscription doesn't exist in Stripe or can't be fetched (e.g., network issues, invalid ID)
|
||||
// Log the issue but proceed with subscription creation to avoid blocking legitimate resubscribe attempts
|
||||
_logger.LogWarning(ex, "Unable to fetch existing subscription {SubscriptionId} for user {UserId}. Proceeding with subscription creation",
|
||||
user.GatewaySubscriptionId, user.Id);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ public interface IGetBitwardenSubscriptionQuery
|
||||
/// Currently only supports <see cref="User"/> subscribers. Future versions will support all
|
||||
/// <see cref="ISubscriber"/> types (User and Organization).
|
||||
/// </remarks>
|
||||
Task<BitwardenSubscription> Run(User user);
|
||||
Task<BitwardenSubscription?> Run(User user);
|
||||
}
|
||||
|
||||
public class GetBitwardenSubscriptionQuery(
|
||||
@@ -39,8 +39,13 @@ public class GetBitwardenSubscriptionQuery(
|
||||
IPricingClient pricingClient,
|
||||
IStripeAdapter stripeAdapter) : IGetBitwardenSubscriptionQuery
|
||||
{
|
||||
public async Task<BitwardenSubscription> Run(User user)
|
||||
public async Task<BitwardenSubscription?> Run(User user)
|
||||
{
|
||||
if (string.IsNullOrEmpty(user.GatewaySubscriptionId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, new SubscriptionGetOptions
|
||||
{
|
||||
Expand =
|
||||
|
||||
Reference in New Issue
Block a user