1
0
mirror of https://github.com/bitwarden/server synced 2025-12-18 17:23:28 +00:00

[PM-27123] Account Credit not Showing for Premium Upgrade Payment (#6484)

* feat(billing): add PaymentMethod union

* feat(billing):  add nontokenized payment method

* feat(billing): add validation for tokinized and nontokenized payments

* feat(billing): update and add payment method requests

* feat(billing): update command with new union object

* test(billing): add tests for account credit for user.

* feat(billing): update premium cloud hosted subscription request

* fix(billing): dotnet format

* tests(billing): include payment method tests

* fix(billing): clean up tests and converter method
This commit is contained in:
Stephon Brown
2025-10-23 14:47:23 -04:00
committed by GitHub
parent b15913ce73
commit ff4b3eb9e5
10 changed files with 391 additions and 37 deletions

View File

@@ -16,6 +16,7 @@ using Microsoft.Extensions.Logging;
using OneOf.Types;
using Stripe;
using Customer = Stripe.Customer;
using PaymentMethod = Bit.Core.Billing.Payment.Models.PaymentMethod;
using Subscription = Stripe.Subscription;
namespace Bit.Core.Billing.Premium.Commands;
@@ -38,7 +39,7 @@ public interface ICreatePremiumCloudHostedSubscriptionCommand
/// <returns>A billing command result indicating success or failure with appropriate error details.</returns>
Task<BillingCommandResult<None>> Run(
User user,
TokenizedPaymentMethod paymentMethod,
PaymentMethod paymentMethod,
BillingAddress billingAddress,
short additionalStorageGb);
}
@@ -60,7 +61,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
public Task<BillingCommandResult<None>> Run(
User user,
TokenizedPaymentMethod paymentMethod,
PaymentMethod paymentMethod,
BillingAddress billingAddress,
short additionalStorageGb) => HandleAsync<None>(async () =>
{
@@ -74,6 +75,7 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
return new BadRequest("Additional storage must be greater than 0.");
}
// Note: A customer will already exist if the customer has purchased account credits.
var customer = string.IsNullOrEmpty(user.GatewayCustomerId)
? await CreateCustomerAsync(user, paymentMethod, billingAddress)
: await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = _expand });
@@ -82,18 +84,31 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
var subscription = await CreateSubscriptionAsync(user.Id, customer, additionalStorageGb > 0 ? additionalStorageGb : null);
switch (paymentMethod)
{
case { Type: TokenizablePaymentMethodType.PayPal }
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
case { Type: not TokenizablePaymentMethodType.PayPal }
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
paymentMethod.Switch(
tokenized =>
{
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (tokenized)
{
case { Type: TokenizablePaymentMethodType.PayPal }
when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete:
case { Type: not TokenizablePaymentMethodType.PayPal }
when subscription.Status == StripeConstants.SubscriptionStatus.Active:
{
user.Premium = true;
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
break;
}
}
},
nonTokenized =>
{
if (subscription.Status == StripeConstants.SubscriptionStatus.Active)
{
user.Premium = true;
user.PremiumExpirationDate = subscription.GetCurrentPeriodEnd();
break;
}
}
});
user.Gateway = GatewayType.Stripe;
user.GatewayCustomerId = customer.Id;
@@ -109,9 +124,15 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
});
private async Task<Customer> CreateCustomerAsync(User user,
TokenizedPaymentMethod paymentMethod,
PaymentMethod paymentMethod,
BillingAddress billingAddress)
{
if (paymentMethod.IsNonTokenized)
{
_logger.LogError("Cannot create customer for user ({UserID}) using non-tokenized payment method. The customer should already exist", user.Id);
throw new BillingException();
}
var subscriberName = user.SubscriberName();
var customerCreateOptions = new CustomerCreateOptions
{
@@ -153,13 +174,14 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
var braintreeCustomerId = "";
// We have checked that the payment method is tokenized, so we can safely cast it.
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (paymentMethod.Type)
switch (paymentMethod.AsT0.Type)
{
case TokenizablePaymentMethodType.BankAccount:
{
var setupIntent =
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.Token }))
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethod.AsT0.Token }))
.FirstOrDefault();
if (setupIntent == null)
@@ -173,19 +195,19 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
}
case TokenizablePaymentMethodType.Card:
{
customerCreateOptions.PaymentMethod = paymentMethod.Token;
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
customerCreateOptions.PaymentMethod = paymentMethod.AsT0.Token;
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethod.AsT0.Token;
break;
}
case TokenizablePaymentMethodType.PayPal:
{
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.Token);
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethod.AsT0.Token);
customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
break;
}
default:
{
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.Type.ToString());
_logger.LogError("Cannot create customer for user ({UserID}) using payment method type ({PaymentMethodType}) as it is not supported", user.Id, paymentMethod.AsT0.Type.ToString());
throw new BillingException();
}
}
@@ -203,18 +225,21 @@ public class CreatePremiumCloudHostedSubscriptionCommand(
async Task Revert()
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (paymentMethod.Type)
if (paymentMethod.IsTokenized)
{
case TokenizablePaymentMethodType.BankAccount:
{
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
break;
}
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
{
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
break;
}
switch (paymentMethod.AsT0.Type)
{
case TokenizablePaymentMethodType.BankAccount:
{
await setupIntentCache.RemoveSetupIntentForSubscriber(user.Id);
break;
}
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
{
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
break;
}
}
}
}
}