1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-06 18:42:49 -05:00

[PM-7452] Handle PayPal for premium users (#4835)

* Add PremiumUserSale

* Add PremiumUserBillingService

* Integrate into UserService behind FF

* Update invoice.created handler to bill newly created PayPal customers

* Run dotnet format
This commit is contained in:
Alex Morask
2024-10-01 09:12:08 -04:00
committed by GitHub
parent 84f7cd262c
commit 594b2a274d
11 changed files with 451 additions and 55 deletions

View File

@ -34,11 +34,9 @@ public class OrganizationBillingService(
{
var (organization, customerSetup, subscriptionSetup) = sale;
List<string> expand = ["tax"];
var customer = customerSetup != null
? await CreateCustomerAsync(organization, customerSetup, expand)
: await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = expand });
var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null
? await CreateCustomerAsync(organization, customerSetup)
: await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax"] });
var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup);
@ -111,31 +109,31 @@ public class OrganizationBillingService(
private async Task<Customer> CreateCustomerAsync(
Organization organization,
CustomerSetup customerSetup,
List<string>? expand = null)
CustomerSetup customerSetup)
{
var organizationDisplayName = organization.DisplayName();
var displayName = organization.DisplayName();
var customerCreateOptions = new CustomerCreateOptions
{
Coupon = customerSetup.Coupon,
Description = organization.DisplayBusinessName(),
Email = organization.BillingEmail,
Expand = expand,
Expand = ["tax"],
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
CustomFields = [
new CustomerInvoiceSettingsCustomFieldOptions
{
Name = organization.SubscriberType(),
Value = organizationDisplayName.Length <= 30
? organizationDisplayName
: organizationDisplayName[..30]
Value = displayName.Length <= 30
? displayName
: displayName[..30]
}]
},
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
["organizationId"] = organization.Id.ToString(),
["region"] = globalSettings.BaseServiceUri.CloudRegion
}
};
@ -174,46 +172,41 @@ public class OrganizationBillingService(
};
customerCreateOptions.TaxIdData = taxIdData;
var (type, token) = customerSetup.TokenizedPaymentSource;
var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource;
// ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault
switch (type)
switch (paymentMethodType)
{
case PaymentMethodType.BankAccount:
{
var setupIntent =
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethodToken }))
.FirstOrDefault();
if (setupIntent == null)
{
logger.LogError("Cannot create customer for organization ({OrganizationID}) without a setup intent for their bank account", organization.Id);
throw new BillingException();
}
await setupIntentCache.Set(organization.Id, setupIntent.Id);
break;
}
case PaymentMethodType.Card:
{
customerCreateOptions.PaymentMethod = token;
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = token;
customerCreateOptions.PaymentMethod = paymentMethodToken;
customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethodToken;
break;
}
case PaymentMethodType.PayPal:
{
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(organization, token);
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(organization, paymentMethodToken);
customerCreateOptions.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
break;
}
default:
{
logger.LogError("Cannot create customer for organization ({OrganizationID}) using payment method type ({PaymentMethodType}) as it is not supported", organization.Id, type.ToString());
logger.LogError("Cannot create customer for organization ({OrganizationID}) using payment method type ({PaymentMethodType}) as it is not supported", organization.Id, paymentMethodType.ToString());
throw new BillingException();
}
}
@ -227,7 +220,6 @@ public class OrganizationBillingService(
StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
{
await Revert();
throw new BadRequestException(
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
}
@ -235,7 +227,6 @@ public class OrganizationBillingService(
StripeConstants.ErrorCodes.TaxIdInvalid)
{
await Revert();
throw new BadRequestException(
"Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.");
}
@ -257,7 +248,7 @@ public class OrganizationBillingService(
await setupIntentCache.Remove(organization.Id);
break;
}
case PaymentMethodType.PayPal:
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
{
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
break;