1
0
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:
Cy Okeke
2026-01-21 12:13:03 +01:00
parent ad19efcff7
commit 2122678664
5 changed files with 70 additions and 39 deletions

View File

@@ -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")]

View File

@@ -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;
}
}

View File

@@ -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:

View File

@@ -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;
}

View File

@@ -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 =