1
0
mirror of https://github.com/bitwarden/server synced 2026-01-31 00:33:17 +00:00

[PM-30855] Pay prorated storage adjustment immediately with Braintree for Premium PayPal users (#6850)

* fix: Pay prorated storage invoice immediately with Braintree for PayPal users

* Run dotnet format
This commit is contained in:
Alex Morask
2026-01-20 09:18:27 -06:00
committed by GitHub
parent c37412bacb
commit 2e4dd061e3
4 changed files with 263 additions and 18 deletions

View File

@@ -2,6 +2,7 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Subscriptions.Models;
using Bit.Core.Entities;
using Bit.Core.Services;
using Bit.Core.Utilities;
@@ -29,6 +30,7 @@ public interface IUpdatePremiumStorageCommand
}
public class UpdatePremiumStorageCommand(
IBraintreeService braintreeService,
IStripeAdapter stripeAdapter,
IUserService userService,
IPricingClient pricingClient,
@@ -49,7 +51,10 @@ public class UpdatePremiumStorageCommand(
// Fetch all premium plans and the user's subscription to find which plan they're on
var premiumPlans = await pricingClient.ListPremiumPlans();
var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId);
var subscription = await stripeAdapter.GetSubscriptionAsync(user.GatewaySubscriptionId, new SubscriptionGetOptions
{
Expand = ["customer"]
});
// Find the password manager subscription item (seat, not storage) and match it to a plan
var passwordManagerItem = subscription.Items.Data.FirstOrDefault(i =>
@@ -127,13 +132,41 @@ public class UpdatePremiumStorageCommand(
});
}
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
Items = subscriptionItemOptions,
ProrationBehavior = ProrationBehavior.AlwaysInvoice
};
var usingPayPal = subscription.Customer.Metadata.ContainsKey(MetadataKeys.BraintreeCustomerId);
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, subscriptionUpdateOptions);
if (usingPayPal)
{
var options = new SubscriptionUpdateOptions
{
Items = subscriptionItemOptions,
ProrationBehavior = ProrationBehavior.CreateProrations
};
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);
var draftInvoice = await stripeAdapter.CreateInvoiceAsync(new InvoiceCreateOptions
{
Customer = subscription.CustomerId,
Subscription = subscription.Id,
AutoAdvance = false,
CollectionMethod = CollectionMethod.ChargeAutomatically
});
var finalizedInvoice = await stripeAdapter.FinalizeInvoiceAsync(draftInvoice.Id,
new InvoiceFinalizeOptions { AutoAdvance = false, Expand = ["customer"] });
await braintreeService.PayInvoice(new UserId(user.Id), finalizedInvoice);
}
else
{
var options = new SubscriptionUpdateOptions
{
Items = subscriptionItemOptions,
ProrationBehavior = ProrationBehavior.AlwaysInvoice
};
await stripeAdapter.UpdateSubscriptionAsync(subscription.Id, options);
}
// Update the user's max storage
user.MaxStorageGb = maxStorageGb;

View File

@@ -24,6 +24,7 @@ public interface IStripeAdapter
Task<Subscription> CancelSubscriptionAsync(string id, SubscriptionCancelOptions options = null);
Task<Invoice> GetInvoiceAsync(string id, InvoiceGetOptions options);
Task<List<Invoice>> ListInvoicesAsync(StripeInvoiceListOptions options);
Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options);
Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options);
Task<List<Invoice>> SearchInvoiceAsync(InvoiceSearchOptions options);
Task<Invoice> UpdateInvoiceAsync(string id, InvoiceUpdateOptions options);

View File

@@ -116,6 +116,9 @@ public class StripeAdapter : IStripeAdapter
return invoices;
}
public Task<Invoice> CreateInvoiceAsync(InvoiceCreateOptions options) =>
_invoiceService.CreateAsync(options);
public Task<Invoice> CreateInvoicePreviewAsync(InvoiceCreatePreviewOptions options) =>
_invoiceService.CreatePreviewAsync(options);