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

[PM-19147] Automatic Tax Improvements (#5545)

* Pm 19147 2 (#5544)

* Pm 19147 2 (#5544)

* Unit tests for tax strategies `GetUpdateOptions`

* Only allow automatic tax flag to be updated for complete subscription updates such as plan changes, not when upgrading additional storage, seats, etc

* unit tests for factory

* Fix build

* Automatic tax for tax estimation

* Fix stub

* Fix stub

* "customer.tax_ids" isn't expanded in some flows.

* Fix SubscriberServiceTests.cs

* BusinessUseAutomaticTaxStrategy > SetUpdateOptions tests

* Fix ProviderBillingServiceTests.cs
This commit is contained in:
Jonas Hendrickx
2025-04-02 19:47:48 +02:00
committed by GitHub
parent 10ea2cb3eb
commit b309de141d
25 changed files with 1448 additions and 108 deletions

View File

@ -1,8 +1,11 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -12,6 +15,7 @@ using Event = Stripe.Event;
namespace Bit.Billing.Services.Implementations;
public class UpcomingInvoiceHandler(
IFeatureService featureService,
ILogger<StripeEventProcessor> logger,
IMailService mailService,
IOrganizationRepository organizationRepository,
@ -21,7 +25,8 @@ public class UpcomingInvoiceHandler(
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand)
IValidateSponsorshipCommand validateSponsorshipCommand,
IAutomaticTaxFactory automaticTaxFactory)
: IUpcomingInvoiceHandler
{
public async Task HandleAsync(Event parsedEvent)
@ -136,6 +141,21 @@ public class UpcomingInvoiceHandler(
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
{
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id));
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
if (updateOptions == null)
{
return;
}
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);
return;
}
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))

View File

@ -47,6 +47,8 @@ public static class StripeConstants
public static class MetadataKeys
{
public const string OrganizationId = "organizationId";
public const string ProviderId = "providerId";
public const string UserId = "userId";
}
public static class PaymentBehavior

View File

@ -21,7 +21,7 @@ public static class CustomerExtensions
/// <param name="customer"></param>
/// <returns></returns>
public static bool HasTaxLocationVerified(this Customer customer) =>
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation;
public static decimal GetBillingBalance(this Customer customer)
{

View File

@ -4,6 +4,7 @@ using Bit.Core.Billing.Licenses.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
namespace Bit.Core.Billing.Extensions;
@ -18,6 +19,9 @@ public static class ServiceCollectionExtensions
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
services.AddTransient<ISubscriberService, SubscriberService>();
services.AddKeyedTransient<IAutomaticTaxStrategy, PersonalUseAutomaticTaxStrategy>(AutomaticTaxFactory.PersonalUse);
services.AddKeyedTransient<IAutomaticTaxStrategy, BusinessUseAutomaticTaxStrategy>(AutomaticTaxFactory.BusinessUse);
services.AddTransient<IAutomaticTaxFactory, AutomaticTaxFactory>();
services.AddLicenseServices();
services.AddPricingClient();
}

View File

@ -1,26 +0,0 @@
using Stripe;
namespace Bit.Core.Billing.Extensions;
public static class SubscriptionCreateOptionsExtensions
{
/// <summary>
/// Attempts to enable automatic tax for given new subscription options.
/// </summary>
/// <param name="options"></param>
/// <param name="customer">The existing customer.</param>
/// <returns>Returns true when successful, false when conditions are not met.</returns>
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;
}
}

View File

@ -0,0 +1,30 @@
#nullable enable
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
namespace Bit.Core.Billing.Services.Contracts;
public class AutomaticTaxFactoryParameters
{
public AutomaticTaxFactoryParameters(PlanType planType)
{
PlanType = planType;
}
public AutomaticTaxFactoryParameters(ISubscriber subscriber, IEnumerable<string> prices)
{
Subscriber = subscriber;
Prices = prices;
}
public AutomaticTaxFactoryParameters(IEnumerable<string> prices)
{
Prices = prices;
}
public ISubscriber? Subscriber { get; init; }
public PlanType? PlanType { get; init; }
public IEnumerable<string>? Prices { get; init; }
}

View File

@ -0,0 +1,11 @@
using Bit.Core.Billing.Services.Contracts;
namespace Bit.Core.Billing.Services;
/// <summary>
/// Responsible for defining the correct automatic tax strategy for either personal use of business use.
/// </summary>
public interface IAutomaticTaxFactory
{
Task<IAutomaticTaxStrategy> CreateAsync(AutomaticTaxFactoryParameters parameters);
}

View File

@ -0,0 +1,33 @@
#nullable enable
using Stripe;
namespace Bit.Core.Billing.Services;
public interface IAutomaticTaxStrategy
{
/// <summary>
///
/// </summary>
/// <param name="subscription"></param>
/// <returns>
/// Returns <see cref="SubscriptionUpdateOptions" /> if changes are to be applied to the subscription, returns null
/// otherwise.
/// </returns>
SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription);
/// <summary>
/// Modifies an existing <see cref="SubscriptionCreateOptions" /> object with the automatic tax flag set correctly.
/// </summary>
/// <param name="options"></param>
/// <param name="customer"></param>
void SetCreateOptions(SubscriptionCreateOptions options, Customer customer);
/// <summary>
/// Modifies an existing <see cref="SubscriptionUpdateOptions" /> object with the automatic tax flag set correctly.
/// </summary>
/// <param name="options"></param>
/// <param name="subscription"></param>
void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription);
void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options);
}

View File

@ -0,0 +1,50 @@
#nullable enable
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Entities;
using Bit.Core.Services;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class AutomaticTaxFactory(
IFeatureService featureService,
IPricingClient pricingClient) : IAutomaticTaxFactory
{
public const string BusinessUse = "business-use";
public const string PersonalUse = "personal-use";
private readonly Lazy<Task<IEnumerable<string>>> _personalUsePlansTask = 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<IAutomaticTaxStrategy> CreateAsync(AutomaticTaxFactoryParameters parameters)
{
if (parameters.Subscriber is User)
{
return new PersonalUseAutomaticTaxStrategy(featureService);
}
if (parameters.PlanType.HasValue)
{
var plan = await pricingClient.GetPlanOrThrow(parameters.PlanType.Value);
return plan.CanBeUsedByBusiness
? new BusinessUseAutomaticTaxStrategy(featureService)
: new PersonalUseAutomaticTaxStrategy(featureService);
}
var personalUsePlans = await _personalUsePlansTask.Value;
if (parameters.Prices != null && parameters.Prices.Any(x => personalUsePlans.Any(y => y == x)))
{
return new PersonalUseAutomaticTaxStrategy(featureService);
}
return new BusinessUseAutomaticTaxStrategy(featureService);
}
}

View File

@ -0,0 +1,96 @@
#nullable enable
using Bit.Core.Billing.Extensions;
using Bit.Core.Services;
using Stripe;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
{
public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription)
{
if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
{
return null;
}
var shouldBeEnabled = ShouldBeEnabled(subscription.Customer);
if (subscription.AutomaticTax.Enabled == shouldBeEnabled)
{
return null;
}
var options = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = shouldBeEnabled
},
DefaultTaxRates = []
};
return options;
}
public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldBeEnabled(customer)
};
}
public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription)
{
if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
{
return;
}
var shouldBeEnabled = ShouldBeEnabled(subscription.Customer);
if (subscription.AutomaticTax.Enabled == shouldBeEnabled)
{
return;
}
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = shouldBeEnabled
};
options.DefaultTaxRates = [];
}
public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options)
{
options.AutomaticTax ??= new InvoiceAutomaticTaxOptions();
if (options.CustomerDetails.Address.Country == "US")
{
options.AutomaticTax.Enabled = true;
return;
}
options.AutomaticTax.Enabled = options.CustomerDetails.TaxIds != null && options.CustomerDetails.TaxIds.Any();
}
private bool ShouldBeEnabled(Customer customer)
{
if (!customer.HasTaxLocationVerified())
{
return false;
}
if (customer.Address.Country == "US")
{
return true;
}
if (customer.TaxIds == null)
{
throw new ArgumentNullException(nameof(customer.TaxIds), "`customer.tax_ids` must be expanded.");
}
return customer.TaxIds.Any();
}
}

View File

@ -0,0 +1,64 @@
#nullable enable
using Bit.Core.Billing.Extensions;
using Bit.Core.Services;
using Stripe;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy
{
public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldBeEnabled(customer)
};
}
public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription)
{
if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
{
return;
}
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldBeEnabled(subscription.Customer)
};
options.DefaultTaxRates = [];
}
public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription)
{
if (!featureService.IsEnabled(FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
{
return null;
}
if (subscription.AutomaticTax.Enabled == ShouldBeEnabled(subscription.Customer))
{
return null;
}
var options = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldBeEnabled(subscription.Customer),
},
DefaultTaxRates = []
};
return options;
}
public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options)
{
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
}
private static bool ShouldBeEnabled(Customer customer)
{
return customer.HasTaxLocationVerified();
}
}

View File

@ -1,9 +1,11 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -23,6 +25,7 @@ namespace Bit.Core.Billing.Services.Implementations;
public class OrganizationBillingService(
IBraintreeGateway braintreeGateway,
IFeatureService featureService,
IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository,
@ -30,7 +33,8 @@ public class OrganizationBillingService(
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
ITaxService taxService) : IOrganizationBillingService
ITaxService taxService,
IAutomaticTaxFactory automaticTaxFactory) : IOrganizationBillingService
{
public async Task Finalize(OrganizationSale sale)
{
@ -143,7 +147,7 @@ public class OrganizationBillingService(
Coupon = customerSetup.Coupon,
Description = organization.DisplayBusinessName(),
Email = organization.BillingEmail,
Expand = ["tax"],
Expand = ["tax", "tax_ids"],
InvoiceSettings = new CustomerInvoiceSettingsOptions
{
CustomFields = [
@ -369,21 +373,8 @@ public class OrganizationBillingService(
}
}
var customerHasTaxInfo = customer is
{
Address:
{
Country: not null and not "",
PostalCode: not null and not ""
}
};
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customerHasTaxInfo
},
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
Customer = customer.Id,
Items = subscriptionItemOptionsList,
@ -395,6 +386,18 @@ public class OrganizationBillingService(
TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays
};
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriptionSetup.PlanType);
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
}
else
{
subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions();
subscriptionCreateOptions.AutomaticTax.Enabled = customer.HasBillingLocation();
}
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -9,6 +10,7 @@ using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Braintree;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stripe;
using Customer = Stripe.Customer;
@ -20,12 +22,14 @@ using static Utilities;
public class PremiumUserBillingService(
IBraintreeGateway braintreeGateway,
IFeatureService featureService,
IGlobalSettings globalSettings,
ILogger<PremiumUserBillingService> logger,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
IUserRepository userRepository) : IPremiumUserBillingService
IUserRepository userRepository,
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy automaticTaxStrategy) : IPremiumUserBillingService
{
public async Task Credit(User user, decimal amount)
{
@ -318,10 +322,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 +335,18 @@ public class PremiumUserBillingService(
OffSession = true
};
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer);
}
else
{
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported,
};
}
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
if (usingPayPal)

View File

@ -1,6 +1,7 @@
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -20,11 +21,13 @@ namespace Bit.Core.Billing.Services.Implementations;
public class SubscriberService(
IBraintreeGateway braintreeGateway,
IFeatureService featureService,
IGlobalSettings globalSettings,
ILogger<SubscriberService> logger,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ITaxService taxService) : ISubscriberService
ITaxService taxService,
IAutomaticTaxFactory automaticTaxFactory) : ISubscriberService
{
public async Task CancelSubscription(
ISubscriber subscriber,
@ -438,7 +441,8 @@ public class SubscriberService(
ArgumentNullException.ThrowIfNull(subscriber);
ArgumentNullException.ThrowIfNull(tokenizedPaymentSource);
var customer = await GetCustomerOrThrow(subscriber);
var customerGetOptions = new CustomerGetOptions { Expand = ["tax", "tax_ids"] };
var customer = await GetCustomerOrThrow(subscriber, customerGetOptions);
var (type, token) = tokenizedPaymentSource;
@ -597,7 +601,7 @@ public class SubscriberService(
Expand = ["subscriptions", "tax", "tax_ids"]
});
await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
customer = await stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
{
Address = new AddressOptions
{
@ -607,7 +611,8 @@ public class SubscriberService(
Line2 = taxInformation.Line2,
City = taxInformation.City,
State = taxInformation.State
}
},
Expand = ["subscriptions", "tax", "tax_ids"]
});
var taxId = customer.TaxIds?.FirstOrDefault();
@ -661,21 +666,42 @@ public class SubscriberService(
}
}
if (SubscriberIsEligibleForAutomaticTax(subscriber, customer))
if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId,
new SubscriptionUpdateOptions
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
{
var subscriptionGetOptions = new SubscriptionGetOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
Expand = ["customer.tax", "customer.tax_ids"]
};
var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id));
var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters);
var automaticTaxOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
if (automaticTaxOptions?.AutomaticTax?.Enabled != null)
{
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, automaticTaxOptions);
}
}
}
else
{
if (SubscriberIsEligibleForAutomaticTax(subscriber, customer))
{
await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId,
new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
return;
return;
bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer)
=> !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) &&
(localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) &&
localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer)
=> !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) &&
(localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) &&
localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
}
}
public async Task VerifyBankAccount(

View File

@ -148,6 +148,8 @@ public static class FeatureFlagKeys
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates";
/* Key Management Team */
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
@ -169,6 +171,7 @@ public static class FeatureFlagKeys
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
/* Platform Team */

View File

@ -9,6 +9,8 @@ using Bit.Core.Billing.Models.Api.Responses;
using Bit.Core.Billing.Models.Business;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Contracts;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
@ -16,6 +18,7 @@ using Bit.Core.Models.BitStripe;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Stripe;
using PaymentMethod = Stripe.PaymentMethod;
@ -36,6 +39,8 @@ public class StripePaymentService : IPaymentService
private readonly ITaxService _taxService;
private readonly ISubscriberService _subscriberService;
private readonly IPricingClient _pricingClient;
private readonly IAutomaticTaxFactory _automaticTaxFactory;
private readonly IAutomaticTaxStrategy _personalUseTaxStrategy;
public StripePaymentService(
ITransactionRepository transactionRepository,
@ -46,7 +51,9 @@ public class StripePaymentService : IPaymentService
IFeatureService featureService,
ITaxService taxService,
ISubscriberService subscriberService,
IPricingClient pricingClient)
IPricingClient pricingClient,
IAutomaticTaxFactory automaticTaxFactory,
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy)
{
_transactionRepository = transactionRepository;
_logger = logger;
@ -57,6 +64,8 @@ public class StripePaymentService : IPaymentService
_taxService = taxService;
_subscriberService = subscriberService;
_pricingClient = pricingClient;
_automaticTaxFactory = automaticTaxFactory;
_personalUseTaxStrategy = personalUseTaxStrategy;
}
private async Task ChangeOrganizationSponsorship(
@ -91,9 +100,7 @@ public class StripePaymentService : IPaymentService
SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)
{
// remember, when in doubt, throw
var subGetOptions = new SubscriptionGetOptions();
// subGetOptions.AddExpand("customer");
subGetOptions.AddExpand("customer.tax");
var subGetOptions = new SubscriptionGetOptions { Expand = ["customer.tax", "customer.tax_ids"] };
var sub = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subGetOptions);
if (sub == null)
{
@ -124,7 +131,19 @@ public class StripePaymentService : IPaymentService
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
}
subUpdateOptions.EnableAutomaticTax(sub.Customer, sub);
if (subscriptionUpdate is CompleteSubscriptionUpdate)
{
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, updatedItemOptions.Select(x => x.Plan ?? x.Price));
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters);
automaticTaxStrategy.SetUpdateOptions(subUpdateOptions, sub);
}
else
{
subUpdateOptions.EnableAutomaticTax(sub.Customer, sub);
}
}
if (!subscriptionUpdate.UpdateNeeded(sub))
{
@ -811,21 +830,46 @@ public class StripePaymentService : IPaymentService
});
}
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
customer.Subscriptions.Any(sub =>
sub.Id == subscriber.GatewaySubscriptionId &&
!sub.AutomaticTax.Enabled) &&
customer.HasTaxLocationVerified())
if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements))
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId))
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
DefaultTaxRates = []
};
var subscriptionGetOptions = new SubscriptionGetOptions
{
Expand = ["customer.tax", "customer.tax_ids"]
};
var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions);
_ = await _stripeAdapter.SubscriptionUpdateAsync(
subscriber.GatewaySubscriptionId,
subscriptionUpdateOptions);
var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id));
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters);
var subscriptionUpdateOptions = automaticTaxStrategy.GetUpdateOptions(subscription);
if (subscriptionUpdateOptions != null)
{
_ = await _stripeAdapter.SubscriptionUpdateAsync(
subscriber.GatewaySubscriptionId,
subscriptionUpdateOptions);
}
}
}
else
{
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
customer.Subscriptions.Any(sub =>
sub.Id == subscriber.GatewaySubscriptionId &&
!sub.AutomaticTax.Enabled) &&
customer.HasTaxLocationVerified())
{
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
DefaultTaxRates = []
};
_ = await _stripeAdapter.SubscriptionUpdateAsync(
subscriber.GatewaySubscriptionId,
subscriptionUpdateOptions);
}
}
}
catch
@ -1214,6 +1258,8 @@ public class StripePaymentService : IPaymentService
}
}
_personalUseTaxStrategy.SetInvoiceCreatePreviewOptions(options);
try
{
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
@ -1256,10 +1302,6 @@ public class StripePaymentService : IPaymentService
var options = new InvoiceCreatePreviewOptions
{
AutomaticTax = new InvoiceAutomaticTaxOptions
{
Enabled = true,
},
Currency = "usd",
SubscriptionDetails = new InvoiceSubscriptionDetailsOptions
{
@ -1347,9 +1389,11 @@ public class StripePaymentService : IPaymentService
];
}
Customer gatewayCustomer = null;
if (!string.IsNullOrWhiteSpace(gatewayCustomerId))
{
var gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
gatewayCustomer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId);
if (gatewayCustomer.Discount != null)
{
@ -1367,6 +1411,10 @@ public class StripePaymentService : IPaymentService
}
}
var automaticTaxFactoryParameters = new AutomaticTaxFactoryParameters(parameters.PasswordManager.Plan);
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxFactoryParameters);
automaticTaxStrategy.SetInvoiceCreatePreviewOptions(options);
try
{
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);