From 594b2a274d3b87add7e4e101e59355f86c80ddde Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 1 Oct 2024 09:12:08 -0400 Subject: [PATCH] [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 --- .../Implementations/InvoiceCreatedHandler.cs | 69 +++-- src/Core/Billing/Constants/StripeConstants.cs | 5 + .../Extensions/ServiceCollectionExtensions.cs | 1 + .../Billing/Models/Sales/PremiumUserSale.cs | 49 ++++ src/Core/Billing/Models/TaxInformation.cs | 12 +- .../Services/IOrganizationBillingService.cs | 4 +- .../Services/IPremiumUserBillingService.cs | 30 ++ .../OrganizationBillingService.cs | 47 ++-- .../PremiumUserBillingService.cs | 260 ++++++++++++++++++ .../Services/Implementations/UserService.cs | 24 +- test/Core.Test/Services/UserServiceTests.cs | 5 +- 11 files changed, 451 insertions(+), 55 deletions(-) create mode 100644 src/Core/Billing/Models/Sales/PremiumUserSale.cs create mode 100644 src/Core/Billing/Services/IPremiumUserBillingService.cs create mode 100644 src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs diff --git a/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs b/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs index 4c84cca96..5bb098bec 100644 --- a/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs +++ b/src/Billing/Services/Implementations/InvoiceCreatedHandler.cs @@ -2,34 +2,63 @@ namespace Bit.Billing.Services.Implementations; -public class InvoiceCreatedHandler : IInvoiceCreatedHandler +public class InvoiceCreatedHandler( + ILogger logger, + IStripeEventService stripeEventService, + IStripeEventUtilityService stripeEventUtilityService, + IProviderEventService providerEventService) + : IInvoiceCreatedHandler { - private readonly IStripeEventService _stripeEventService; - private readonly IStripeEventUtilityService _stripeEventUtilityService; - private readonly IProviderEventService _providerEventService; - - public InvoiceCreatedHandler( - IStripeEventService stripeEventService, - IStripeEventUtilityService stripeEventUtilityService, - IProviderEventService providerEventService) - { - _stripeEventService = stripeEventService; - _stripeEventUtilityService = stripeEventUtilityService; - _providerEventService = providerEventService; - } /// - /// Handles the event type from Stripe. + /// + /// This handler processes the `invoice.created` event in Stripe. It has + /// two primary responsibilities. + /// + /// + /// 1. Checks to see if the newly created invoice belongs to a PayPal customer. If it does, and the invoice is ready to be paid, it will attempt to pay the invoice + /// with Braintree and then let Stripe know the invoice can be marked as paid. + /// + /// + /// 2. If the invoice is for a provider, it records a point-in-time snapshot of the invoice broken down by the provider's client organizations. This is later used in + /// the provider invoice export. + /// /// - /// public async Task HandleAsync(Event parsedEvent) { - var invoice = await _stripeEventService.GetInvoice(parsedEvent, true); - if (_stripeEventUtilityService.ShouldAttemptToPayInvoice(invoice)) + try { - await _stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice); + var invoice = await stripeEventService.GetInvoice(parsedEvent, true, ["customer"]); + + var usingPayPal = invoice.Customer?.Metadata.ContainsKey("btCustomerId") ?? false; + + if (usingPayPal && invoice is + { + AmountDue: > 0, + Paid: false, + CollectionMethod: "charge_automatically", + BillingReason: + "subscription_create" or + "subscription_cycle" or + "automatic_pending_invoice_item_invoice", + SubscriptionId: not null and not "" + }) + { + await stripeEventUtilityService.AttemptToPayInvoiceAsync(invoice); + } + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to attempt paying for invoice while handling 'invoice.created' event ({EventID})", parsedEvent.Id); } - await _providerEventService.TryRecordInvoiceLineItems(parsedEvent); + try + { + await providerEventService.TryRecordInvoiceLineItems(parsedEvent); + } + catch (Exception exception) + { + logger.LogError(exception, "Failed to record provider invoice line items while handling 'invoice.created' event ({EventID})", parsedEvent.Id); + } } } diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 9279a0854..44cda35b7 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -28,6 +28,11 @@ public static class StripeConstants public const string TaxIdInvalid = "tax_id_invalid"; } + public static class PaymentBehavior + { + public const string DefaultIncomplete = "default_incomplete"; + } + public static class PaymentMethodTypes { public const string Card = "card"; diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index ffe5cc3ed..abfceac73 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -12,6 +12,7 @@ public static class ServiceCollectionExtensions public static void AddBillingOperations(this IServiceCollection services) { services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); } diff --git a/src/Core/Billing/Models/Sales/PremiumUserSale.cs b/src/Core/Billing/Models/Sales/PremiumUserSale.cs new file mode 100644 index 000000000..6bc054eac --- /dev/null +++ b/src/Core/Billing/Models/Sales/PremiumUserSale.cs @@ -0,0 +1,49 @@ +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business; + +namespace Bit.Core.Billing.Models.Sales; + +#nullable enable + +public class PremiumUserSale +{ + private PremiumUserSale() { } + + public required User User { get; set; } + public required CustomerSetup CustomerSetup { get; set; } + public short? Storage { get; set; } + + public void Deconstruct( + out User user, + out CustomerSetup customerSetup, + out short? storage) + { + user = User; + customerSetup = CustomerSetup; + storage = Storage; + } + + public static PremiumUserSale From( + User user, + PaymentMethodType paymentMethodType, + string paymentMethodToken, + TaxInfo taxInfo, + short? storage) + { + var tokenizedPaymentSource = new TokenizedPaymentSource(paymentMethodType, paymentMethodToken); + + var taxInformation = TaxInformation.From(taxInfo); + + return new PremiumUserSale + { + User = user, + CustomerSetup = new CustomerSetup + { + TokenizedPaymentSource = tokenizedPaymentSource, + TaxInformation = taxInformation + }, + Storage = storage + }; + } +} diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Models/TaxInformation.cs index 7c03b90f6..5403f9469 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Models/TaxInformation.cs @@ -1,4 +1,5 @@ -using Stripe; +using Bit.Core.Models.Business; +using Stripe; namespace Bit.Core.Billing.Models; @@ -11,6 +12,15 @@ public record TaxInformation( string City, string State) { + public static TaxInformation From(TaxInfo taxInfo) => new( + taxInfo.BillingAddressCountry, + taxInfo.BillingAddressPostalCode, + taxInfo.TaxIdNumber, + taxInfo.BillingAddressLine1, + taxInfo.BillingAddressLine2, + taxInfo.BillingAddressCity, + taxInfo.BillingAddressState); + public (AddressOptions, List) GetStripeOptions() { var address = new AddressOptions diff --git a/src/Core/Billing/Services/IOrganizationBillingService.cs b/src/Core/Billing/Services/IOrganizationBillingService.cs index 907860d96..db62d545e 100644 --- a/src/Core/Billing/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Services/IOrganizationBillingService.cs @@ -9,7 +9,7 @@ namespace Bit.Core.Billing.Services; public interface IOrganizationBillingService { /// - /// Establishes the billing configuration for a Bitwarden using the provided . + /// Establishes the Stripe entities necessary for a Bitwarden using the provided . /// /// The method first checks to see if the /// provided already has a Stripe using the . @@ -17,7 +17,7 @@ public interface IOrganizationBillingService /// for the created or existing customer using the provided . /// /// - /// The purchase details necessary to establish the Stripe entities responsible for billing the organization. + /// The data required to establish the Stripe entities responsible for billing the organization. /// /// /// var sale = OrganizationSale.From(organization, organizationSignup); diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs new file mode 100644 index 000000000..f74bf6c8d --- /dev/null +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -0,0 +1,30 @@ +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Entities; + +namespace Bit.Core.Billing.Services; + +public interface IPremiumUserBillingService +{ + /// + /// Establishes the Stripe entities necessary for a Bitwarden using the provided . + /// + /// The method first checks to see if the + /// provided already has a Stripe using the . + /// If it doesn't, the method creates one using the 's . The method then creates a Stripe + /// for the created or existing customer while appending the provided 's . + /// + /// + /// The data required to establish the Stripe entities responsible for billing the premium user. + /// + /// + /// var sale = PremiumUserSale.From( + /// user, + /// paymentMethodType, + /// paymentMethodToken, + /// taxInfo, + /// storage); + /// await premiumUserBillingService.Finalize(sale); + /// + /// + Task Finalize(PremiumUserSale sale); +} diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 0880c3678..3c5938cab 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -34,11 +34,9 @@ public class OrganizationBillingService( { var (organization, customerSetup, subscriptionSetup) = sale; - List 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 CreateCustomerAsync( Organization organization, - CustomerSetup customerSetup, - List? 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 { - { "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; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs new file mode 100644 index 000000000..92c81dae1 --- /dev/null +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -0,0 +1,260 @@ +using Bit.Core.Billing.Caches; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Braintree; +using Microsoft.Extensions.Logging; +using Stripe; +using Customer = Stripe.Customer; +using Subscription = Stripe.Subscription; + +namespace Bit.Core.Billing.Services.Implementations; + +using static Utilities; + +public class PremiumUserBillingService( + IBraintreeGateway braintreeGateway, + IGlobalSettings globalSettings, + ILogger logger, + ISetupIntentCache setupIntentCache, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService, + IUserRepository userRepository) : IPremiumUserBillingService +{ + public async Task Finalize(PremiumUserSale sale) + { + var (user, customerSetup, storage) = sale; + + List expand = ["tax"]; + + var customer = string.IsNullOrEmpty(user.GatewayCustomerId) + ? await CreateCustomerAsync(user, customerSetup) + : await subscriberService.GetCustomerOrThrow(user, new CustomerGetOptions { Expand = expand }); + + var subscription = await CreateSubscriptionAsync(user.Id, customer, storage); + + switch (customerSetup.TokenizedPaymentSource) + { + case { Type: PaymentMethodType.PayPal } + when subscription.Status == StripeConstants.SubscriptionStatus.Incomplete: + case { Type: not PaymentMethodType.PayPal } + when subscription.Status == StripeConstants.SubscriptionStatus.Active: + { + user.Premium = true; + user.PremiumExpirationDate = subscription.CurrentPeriodEnd; + break; + } + } + + user.Gateway = GatewayType.Stripe; + user.GatewayCustomerId = customer.Id; + user.GatewaySubscriptionId = subscription.Id; + + await userRepository.ReplaceAsync(user); + } + + private async Task CreateCustomerAsync( + User user, + CustomerSetup customerSetup) + { + if (customerSetup.TokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) + { + logger.LogError( + "Cannot create customer for user ({UserID}) without a valid payment source", user.Id); + + throw new BillingException(); + } + + if (customerSetup.TaxInformation is not { Country: not null and not "", PostalCode: not null and not "" }) + { + logger.LogError( + "Cannot create customer for user ({UserID}) without valid tax information", user.Id); + + throw new BillingException(); + } + + var (address, taxIdData) = customerSetup.TaxInformation.GetStripeOptions(); + + var subscriberName = user.SubscriberName(); + + var customerCreateOptions = new CustomerCreateOptions + { + Address = address, + Description = user.Name, + Email = user.Email, + Expand = ["tax"], + InvoiceSettings = new CustomerInvoiceSettingsOptions + { + CustomFields = + [ + new CustomerInvoiceSettingsCustomFieldOptions + { + Name = user.SubscriberType(), + Value = subscriberName.Length <= 30 + ? subscriberName + : subscriberName[..30] + } + ] + }, + Metadata = new Dictionary + { + ["region"] = globalSettings.BaseServiceUri.CloudRegion, + ["userId"] = user.Id.ToString() + }, + Tax = new CustomerTaxOptions + { + ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately + }, + TaxIdData = taxIdData + }; + + var (paymentMethodType, paymentMethodToken) = customerSetup.TokenizedPaymentSource; + + var braintreeCustomerId = ""; + + // ReSharper disable once SwitchStatementHandlesSomeKnownEnumValuesWithDefault + switch (paymentMethodType) + { + case PaymentMethodType.BankAccount: + { + var setupIntent = + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = paymentMethodToken })) + .FirstOrDefault(); + + if (setupIntent == null) + { + logger.LogError("Cannot create customer for user ({UserID}) without a setup intent for their bank account", user.Id); + throw new BillingException(); + } + + await setupIntentCache.Set(user.Id, setupIntent.Id); + break; + } + case PaymentMethodType.Card: + { + customerCreateOptions.PaymentMethod = paymentMethodToken; + customerCreateOptions.InvoiceSettings.DefaultPaymentMethod = paymentMethodToken; + break; + } + case PaymentMethodType.PayPal: + { + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(user, paymentMethodToken); + 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, paymentMethodType.ToString()); + throw new BillingException(); + } + } + + try + { + return await stripeAdapter.CustomerCreateAsync(customerCreateOptions); + } + catch (StripeException stripeException) when (stripeException.StripeError?.Code == + StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) + { + await Revert(); + throw new BadRequestException( + "Your location wasn't recognized. Please ensure your country and postal code are valid."); + } + catch (StripeException stripeException) when (stripeException.StripeError?.Code == + 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."); + } + catch + { + await Revert(); + throw; + } + + async Task Revert() + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (customerSetup.TokenizedPaymentSource!.Type) + { + case PaymentMethodType.BankAccount: + { + await setupIntentCache.Remove(user.Id); + break; + } + case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } + } + } + } + + private async Task CreateSubscriptionAsync( + Guid userId, + Customer customer, + int? storage) + { + var subscriptionItemOptionsList = new List + { + new () + { + Price = "premium-annually", + Quantity = 1 + } + }; + + if (storage is > 0) + { + subscriptionItemOptionsList.Add(new SubscriptionItemOptions + { + Price = "storage-gb-annually", + Quantity = storage + }); + } + + var usingPayPal = customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) ?? false; + + var subscriptionCreateOptions = new SubscriptionCreateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, + }, + CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, + Customer = customer.Id, + Items = subscriptionItemOptionsList, + Metadata = new Dictionary + { + ["userId"] = userId.ToString() + }, + PaymentBehavior = usingPayPal + ? StripeConstants.PaymentBehavior.DefaultIncomplete + : null, + OffSession = true + }; + + var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); + + if (usingPayPal) + { + await stripeAdapter.InvoiceUpdateAsync(subscription.LatestInvoiceId, new InvoiceUpdateOptions + { + AutoAdvance = false + }); + } + + return subscription; + } +} diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 87fdd75fe..fe04efa22 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -61,6 +63,8 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IProviderUserRepository _providerUserRepository; private readonly IStripeSyncService _stripeSyncService; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IPremiumUserBillingService _premiumUserBillingService; public UserService( IUserRepository userRepository, @@ -92,7 +96,9 @@ public class UserService : UserManager, IUserService, IDisposable IAcceptOrgUserCommand acceptOrgUserCommand, IProviderUserRepository providerUserRepository, IStripeSyncService stripeSyncService, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IPremiumUserBillingService premiumUserBillingService) : base( store, optionsAccessor, @@ -130,6 +136,8 @@ public class UserService : UserManager, IUserService, IDisposable _providerUserRepository = providerUserRepository; _stripeSyncService = stripeSyncService; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _premiumUserBillingService = premiumUserBillingService; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -904,8 +912,18 @@ public class UserService : UserManager, IUserService, IDisposable } else { - paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, - paymentToken, additionalStorageGb, taxInfo); + var deprecateStripeSourcesAPI = _featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI); + + if (deprecateStripeSourcesAPI) + { + var sale = PremiumUserSale.From(user, paymentMethodType, paymentToken, taxInfo, additionalStorageGb); + await _premiumUserBillingService.Finalize(sale); + } + else + { + paymentIntentClientSecret = await _paymentService.PurchasePremiumAsync(user, paymentMethodType, + paymentToken, additionalStorageGb, taxInfo); + } } user.Premium = true; diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 1c727adee..098d4d279 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -260,7 +261,9 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - new FakeDataProtectorTokenFactory() + new FakeDataProtectorTokenFactory(), + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret);