diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index d2acdac079..841ce74b81 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -28,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly ISubscriberService _subscriberService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; + private readonly IOrganizationAutomaticTaxStrategy _organizationAutomaticTaxStrategy; public RemoveOrganizationFromProviderCommand( IEventService eventService, @@ -40,7 +41,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv IProviderBillingService providerBillingService, ISubscriberService subscriberService, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IPricingClient pricingClient) + IPricingClient pricingClient, + IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy) { _eventService = eventService; _mailService = mailService; @@ -53,6 +55,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _subscriberService = subscriberService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; + _organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy; } public async Task RemoveOrganizationFromProvider( @@ -107,7 +110,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv organization.IsValidClient() && !string.IsNullOrEmpty(organization.GatewayCustomerId)) { - await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions + var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions { Description = string.Empty, Email = organization.BillingEmail @@ -120,7 +123,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Customer = organization.GatewayCustomerId, CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, DaysUntilDue = 30, - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, Metadata = new Dictionary { { "organizationId", organization.Id.ToString() } @@ -130,6 +132,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] }; + await _organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer); + var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); organization.GatewaySubscriptionId = subscription.Id; diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 294a926022..508f0a0d6f 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -40,7 +40,8 @@ public class ProviderBillingService( IProviderUserRepository providerUserRepository, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - ITaxService taxService) : IProviderBillingService + ITaxService taxService, + IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy) : IProviderBillingService { [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] public async Task AddExistingOrganization( @@ -589,10 +590,6 @@ public class ProviderBillingService( var subscriptionCreateOptions = new SubscriptionCreateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }, CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, Customer = customer.Id, DaysUntilDue = 30, @@ -605,6 +602,8 @@ public class ProviderBillingService( ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations }; + await organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer); + try { var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); diff --git a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs deleted file mode 100644 index d76a0553a3..0000000000 --- a/src/Core/Billing/Extensions/SubscriptionCreateOptionsExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class SubscriptionCreateOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given new subscription options. - /// - /// - /// The existing customer. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax(this SubscriptionCreateOptions options, Customer customer) - { - // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.DefaultTaxRates = []; - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - - return true; - } -} diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs deleted file mode 100644 index d70af78fa8..0000000000 --- a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Stripe; - -namespace Bit.Core.Billing.Extensions; - -public static class SubscriptionUpdateOptionsExtensions -{ - /// - /// Attempts to enable automatic tax for given subscription options. - /// - /// - /// The existing customer to which the subscription belongs. - /// The existing subscription. - /// Returns true when successful, false when conditions are not met. - public static bool EnableAutomaticTax( - this SubscriptionUpdateOptions options, - Customer customer, - Subscription subscription) - { - if (subscription.AutomaticTax.Enabled) - { - return false; - } - - // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) - { - return false; - } - - options.DefaultTaxRates = []; - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; - - return true; - } -} diff --git a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs b/src/Core/Billing/Services/IAutomaticTaxStrategy.cs index 26f6fd989d..628e241d8f 100644 --- a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Services/IAutomaticTaxStrategy.cs @@ -5,6 +5,6 @@ namespace Bit.Core.Billing.Services; public interface IAutomaticTaxStrategy { Task GetUpdateOptionsAsync(Subscription subscription); - Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null); + Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer); Task SetUpdateOptionsAsync(SubscriptionUpdateOptions options, Subscription subscription); } diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/IndividualAutomaticTaxStrategy.cs b/src/Core/Billing/Services/Implementations/AutomaticTax/IndividualAutomaticTaxStrategy.cs index f28398b84c..b0bc63c150 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/IndividualAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Services/Implementations/AutomaticTax/IndividualAutomaticTaxStrategy.cs @@ -1,13 +1,20 @@ -using Stripe; +using Bit.Core.Billing.Constants; +using Stripe; namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy { - public Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null) + public Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer) { ArgumentNullException.ThrowIfNull(options); - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + ArgumentNullException.ThrowIfNull(customer); + + options.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = ShouldEnable(customer) + }; + return Task.CompletedTask; } @@ -16,18 +23,23 @@ public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy ArgumentNullException.ThrowIfNull(options); - if (subscription.AutomaticTax.Enabled) + if (subscription.AutomaticTax.Enabled == ShouldEnable(subscription.Customer)) { return Task.CompletedTask; } - options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + options.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = ShouldEnable(subscription.Customer) + }; + options.DefaultTaxRates = []; + return Task.CompletedTask; } public Task GetUpdateOptionsAsync(Subscription subscription) { - if (subscription.AutomaticTax.Enabled) + if (subscription.AutomaticTax.Enabled == ShouldEnable(subscription.Customer)) { return null; } @@ -36,10 +48,16 @@ public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy { AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true - } + Enabled = ShouldEnable(subscription.Customer), + }, + DefaultTaxRates = [] }; return Task.FromResult(options); } + + private static bool ShouldEnable(Customer customer) + { + return customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; + } } diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/OrganizationAutomaticTaxStrategy.cs b/src/Core/Billing/Services/Implementations/AutomaticTax/OrganizationAutomaticTaxStrategy.cs index dfdbfd04b4..5852c7256c 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/OrganizationAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Services/Implementations/AutomaticTax/OrganizationAutomaticTaxStrategy.cs @@ -8,6 +8,15 @@ namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; public class OrganizationAutomaticTaxStrategy( IPricingClient pricingClient) : IOrganizationAutomaticTaxStrategy { + private readonly Lazy>> _familyPriceIdsTask = new(async () => + { + var plans = await Task.WhenAll( + pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), + pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)); + + return plans.Select(plan => plan.PasswordManager.StripePlanId); + }); + public async Task GetUpdateOptionsAsync(Subscription subscription) { ArgumentNullException.ThrowIfNull(subscription); @@ -23,13 +32,14 @@ public class OrganizationAutomaticTaxStrategy( AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isEnabled.Value - } + }, + DefaultTaxRates = [] }; return options; } - public async Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null) + public async Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer) { ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(customer); @@ -59,6 +69,7 @@ public class OrganizationAutomaticTaxStrategy( { Enabled = isEnabled.Value }; + options.DefaultTaxRates = []; } private async Task IsEnabledAsync(Subscription subscription) @@ -75,13 +86,9 @@ public class OrganizationAutomaticTaxStrategy( private async Task IsNonTaxableNonUsBusinessUseSubscriptionAsync(Subscription subscription) { - var familyPriceIds = (await Task.WhenAll( - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) - .Select(plan => plan.PasswordManager.StripePlanId); + var familyPriceIds = await _familyPriceIdsTask.Value; return subscription.Customer.Address.Country != "US" && - subscription.IsOrganization() && !subscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() && !subscription.Customer.TaxIds.Any(); } @@ -99,10 +106,7 @@ public class OrganizationAutomaticTaxStrategy( private async Task IsNonTaxableNonUsBusinessUseSubscriptionAsync(SubscriptionCreateOptions options, Customer customer) { - var familyPriceIds = (await Task.WhenAll( - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) - .Select(plan => plan.PasswordManager.StripePlanId); + var familyPriceIds = await _familyPriceIdsTask.Value; return customer.Address.Country != "US" && !options.Items.Select(item => item.Price).Intersect(familyPriceIds).Any() && diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 57be92ba94..ff9bbaaca5 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -25,7 +25,8 @@ public class PremiumUserBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository) : IPremiumUserBillingService + IUserRepository userRepository, + IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy) : IPremiumUserBillingService { public async Task Credit(User user, decimal amount) { @@ -318,10 +319,6 @@ public class PremiumUserBillingService( var subscriptionCreateOptions = new SubscriptionCreateOptions { - AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, - }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, Items = subscriptionItemOptionsList, @@ -335,6 +332,8 @@ public class PremiumUserBillingService( OffSession = true }; + await individualAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer); + var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); if (usingPayPal) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index ca377407f4..3b36c693cb 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -36,6 +36,8 @@ public class StripePaymentService : IPaymentService private readonly ITaxService _taxService; private readonly ISubscriberService _subscriberService; private readonly IPricingClient _pricingClient; + private readonly IIndividualAutomaticTaxStrategy _individualAutomaticTaxStrategy; + private readonly IOrganizationAutomaticTaxStrategy _organizationAutomaticTaxStrategy; public StripePaymentService( ITransactionRepository transactionRepository, @@ -46,7 +48,9 @@ public class StripePaymentService : IPaymentService IFeatureService featureService, ITaxService taxService, ISubscriberService subscriberService, - IPricingClient pricingClient) + IPricingClient pricingClient, + IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy, + IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy) { _transactionRepository = transactionRepository; _logger = logger; @@ -57,6 +61,8 @@ public class StripePaymentService : IPaymentService _taxService = taxService; _subscriberService = subscriberService; _pricingClient = pricingClient; + _individualAutomaticTaxStrategy = individualAutomaticTaxStrategy; + _organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy; } private async Task ChangeOrganizationSponsorship( @@ -124,7 +130,7 @@ public class StripePaymentService : IPaymentService new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; } - subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); + await _organizationAutomaticTaxStrategy.SetUpdateOptionsAsync(subUpdateOptions, sub); if (!subscriptionUpdate.UpdateNeeded(sub)) { @@ -812,20 +818,20 @@ public class StripePaymentService : IPaymentService } if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && - customer.Subscriptions.Any(sub => - sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled) && - customer.HasTaxLocationVerified()) + customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId)) { - var subscriptionUpdateOptions = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - DefaultTaxRates = [] - }; + var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId); - _ = await _stripeAdapter.SubscriptionUpdateAsync( - subscriber.GatewaySubscriptionId, - subscriptionUpdateOptions); + var subscriptionUpdateOptions = subscriber is User + ? await _individualAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription) + : await _organizationAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription); + + if (subscriptionUpdateOptions != null) + { + _ = await _stripeAdapter.SubscriptionUpdateAsync( + subscriber.GatewaySubscriptionId, + subscriptionUpdateOptions); + } } } catch