mirror of
https://github.com/bitwarden/server
synced 2026-02-22 12:23:37 +00:00
Implement the correct changes
This commit is contained in:
@@ -102,7 +102,7 @@ public class AccountBillingVNextController(
|
||||
[BindNever] User user)
|
||||
{
|
||||
var subscription = await getBitwardenSubscriptionQuery.Run(user);
|
||||
return TypedResults.Ok(subscription);
|
||||
return subscription == null ? TypedResults.NotFound() : TypedResults.Ok(subscription);
|
||||
}
|
||||
|
||||
[HttpPost("subscription/reinstate")]
|
||||
|
||||
@@ -104,26 +104,47 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();
|
||||
|
||||
if (unpaidSubscriptions == null || unpaidSubscriptions.Count == 0)
|
||||
var incompleteSubscriptions = subscriptions?.Data.Where(subscription =>
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Incomplete).ToList();
|
||||
|
||||
// Process unpaid subscriptions
|
||||
if (unpaidSubscriptions != null && unpaidSubscriptions.Count > 0)
|
||||
{
|
||||
foreach (var subscription in unpaidSubscriptions)
|
||||
{
|
||||
await AttemptToPayOpenSubscriptionAsync(subscription);
|
||||
}
|
||||
}
|
||||
|
||||
// Process incomplete subscriptions - only if there's exactly one to avoid overcharging
|
||||
if (incompleteSubscriptions == null || incompleteSubscriptions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unpaidSubscription in unpaidSubscriptions)
|
||||
{
|
||||
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription)
|
||||
{
|
||||
var latestInvoice = unpaidSubscription.LatestInvoice;
|
||||
|
||||
if (unpaidSubscription.LatestInvoice is null)
|
||||
if (incompleteSubscriptions.Count > 1)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice didn't exist",
|
||||
unpaidSubscription.Id);
|
||||
"Customer {CustomerId} has {Count} incomplete subscriptions. Skipping automatic payment retry to avoid overcharging. Subscription IDs: {SubscriptionIds}",
|
||||
customer.Id,
|
||||
incompleteSubscriptions.Count,
|
||||
string.Join(", ", incompleteSubscriptions.Select(s => s.Id)));
|
||||
return;
|
||||
}
|
||||
|
||||
// Exactly one incomplete subscription - safe to retry
|
||||
await AttemptToPayOpenSubscriptionAsync(incompleteSubscriptions.First());
|
||||
}
|
||||
|
||||
private async Task AttemptToPayOpenSubscriptionAsync(Subscription subscription)
|
||||
{
|
||||
var latestInvoice = subscription.LatestInvoice;
|
||||
|
||||
if (subscription.LatestInvoice is null)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice didn't exist",
|
||||
subscription.Id, subscription.Status);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -131,8 +152,8 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
if (latestInvoice.Status != StripeInvoiceStatus.Open)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to pay unpaid subscription {SubscriptionId} but latest invoice wasn't \"open\"",
|
||||
unpaidSubscription.Id);
|
||||
"Attempted to pay subscription {SubscriptionId} with status {Status} but latest invoice wasn't \"open\"",
|
||||
subscription.Id, subscription.Status);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -144,8 +165,8 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Attempted to pay open invoice {InvoiceId} on unpaid subscription {SubscriptionId} but encountered an error",
|
||||
latestInvoice.Id, unpaidSubscription.Id);
|
||||
"Attempted to pay open invoice {InvoiceId} on subscription {SubscriptionId} with status {Status} but encountered an error",
|
||||
latestInvoice.Id, subscription.Id, subscription.Status);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,20 +117,6 @@ 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 (await IsPremiumSubscriptionAsync(subscription) &&
|
||||
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:
|
||||
|
||||
@@ -72,7 +72,23 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
BillingAddress billingAddress,
|
||||
short additionalStorageGb) => HandleAsync<None>(async () =>
|
||||
{
|
||||
if (user.Premium)
|
||||
var hasTerminalSubscription = false;
|
||||
if (!string.IsNullOrEmpty(user.GatewaySubscriptionId))
|
||||
{
|
||||
try
|
||||
{
|
||||
var existingSubscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
|
||||
hasTerminalSubscription = existingSubscription.Status is
|
||||
SubscriptionStatus.Canceled or
|
||||
SubscriptionStatus.IncompleteExpired;
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Subscription doesn't exist or can't be fetched, proceed as normal
|
||||
}
|
||||
}
|
||||
|
||||
if (user.Premium && !hasTerminalSubscription)
|
||||
{
|
||||
return new BadRequest("Already a premium user.");
|
||||
}
|
||||
@@ -98,8 +114,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 +141,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();
|
||||
@@ -132,7 +151,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
|
||||
},
|
||||
_ =>
|
||||
{
|
||||
if (subscription.Status != SubscriptionStatus.Active)
|
||||
if (subscription.Status is not (SubscriptionStatus.Active or SubscriptionStatus.Incomplete))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,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(
|
||||
@@ -38,8 +38,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