mirror of
https://github.com/bitwarden/server
synced 2026-01-04 09:33:40 +00:00
[PM-24350] fix tax calculation (#6251)
This commit is contained in:
@@ -22,6 +22,19 @@ public static class BillingExtensions
|
||||
_ => throw new BillingException($"PlanType {planType} could not be matched to a ProductTierType")
|
||||
};
|
||||
|
||||
public static bool IsBusinessProductTierType(this PlanType planType)
|
||||
=> IsBusinessProductTierType(planType.GetProductTier());
|
||||
|
||||
public static bool IsBusinessProductTierType(this ProductTierType productTierType)
|
||||
=> productTierType switch
|
||||
{
|
||||
ProductTierType.Free => false,
|
||||
ProductTierType.Families => false,
|
||||
ProductTierType.Enterprise => true,
|
||||
ProductTierType.Teams => true,
|
||||
ProductTierType.TeamsStarter => true
|
||||
};
|
||||
|
||||
public static bool IsBillable(this Provider provider) =>
|
||||
provider is
|
||||
{
|
||||
|
||||
@@ -25,9 +25,6 @@ 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();
|
||||
services.AddTransient<IPreviewTaxAmountCommand, PreviewTaxAmountCommand>();
|
||||
|
||||
@@ -275,7 +275,7 @@ public class OrganizationBillingService(
|
||||
|
||||
|
||||
if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families &&
|
||||
customerSetup.TaxInformation.Country != "US")
|
||||
customerSetup.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
@@ -514,14 +514,14 @@ public class OrganizationBillingService(
|
||||
|
||||
customer = customer switch
|
||||
{
|
||||
{ Address.Country: not "US", TaxExempt: not StripeConstants.TaxExempt.Reverse } => await
|
||||
{ Address.Country: not Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: not StripeConstants.TaxExempt.Reverse } => await
|
||||
stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
Expand = expansions,
|
||||
TaxExempt = StripeConstants.TaxExempt.Reverse
|
||||
}),
|
||||
{ Address.Country: "US", TaxExempt: StripeConstants.TaxExempt.Reverse } => await
|
||||
{ Address.Country: Core.Constants.CountryAbbreviations.UnitedStates, TaxExempt: StripeConstants.TaxExempt.Reverse } => await
|
||||
stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
new CustomerUpdateOptions
|
||||
{
|
||||
|
||||
@@ -84,7 +84,7 @@ public class UpdateBillingAddressCommand(
|
||||
State = billingAddress.State
|
||||
},
|
||||
Expand = ["subscriptions", "tax_ids"],
|
||||
TaxExempt = billingAddress.Country != "US"
|
||||
TaxExempt = billingAddress.Country != Core.Constants.CountryAbbreviations.UnitedStates
|
||||
? StripeConstants.TaxExempt.Reverse
|
||||
: StripeConstants.TaxExempt.None
|
||||
});
|
||||
|
||||
@@ -801,15 +801,13 @@ public class SubscriberService(
|
||||
_ => false
|
||||
};
|
||||
|
||||
|
||||
|
||||
if (isBusinessUseSubscriber)
|
||||
{
|
||||
switch (customer)
|
||||
{
|
||||
case
|
||||
{
|
||||
Address.Country: not "US",
|
||||
Address.Country: not Core.Constants.CountryAbbreviations.UnitedStates,
|
||||
TaxExempt: not TaxExempt.Reverse
|
||||
}:
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
@@ -817,7 +815,7 @@ public class SubscriberService(
|
||||
break;
|
||||
case
|
||||
{
|
||||
Address.Country: "US",
|
||||
Address.Country: Core.Constants.CountryAbbreviations.UnitedStates,
|
||||
TaxExempt: TaxExempt.Reverse
|
||||
}:
|
||||
await stripeAdapter.CustomerUpdateAsync(customer.Id,
|
||||
@@ -840,8 +838,8 @@ public class SubscriberService(
|
||||
{
|
||||
User => true,
|
||||
Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families ||
|
||||
customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false),
|
||||
Provider => customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false),
|
||||
customer.Address.Country == Core.Constants.CountryAbbreviations.UnitedStates || (customer.TaxIds?.Any() ?? false),
|
||||
Provider => customer.Address.Country == Core.Constants.CountryAbbreviations.UnitedStates || (customer.TaxIds?.Any() ?? false),
|
||||
_ => false
|
||||
};
|
||||
|
||||
|
||||
@@ -95,17 +95,11 @@ public class PreviewTaxAmountCommand(
|
||||
}
|
||||
}
|
||||
|
||||
if (planType.GetProductTier() == ProductTierType.Families)
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
if (parameters.PlanType.IsBusinessProductTierType() &&
|
||||
parameters.TaxInformation.Country != Core.Constants.CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
else
|
||||
{
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions
|
||||
{
|
||||
Enabled = options.CustomerDetails.Address.Country == "US" ||
|
||||
options.CustomerDetails.TaxIds is [_, ..]
|
||||
};
|
||||
options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.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);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
#nullable enable
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.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);
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
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.HasRecognizedTaxLocation())
|
||||
{
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
#nullable enable
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Core.Billing.Tax.Services.Implementations;
|
||||
|
||||
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.HasRecognizedTaxLocation();
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,19 @@ public static class Constants
|
||||
/// regardless of whether there is a proration or not.
|
||||
/// </summary>
|
||||
public const string AlwaysInvoice = "always_invoice";
|
||||
|
||||
/// <summary>
|
||||
/// Used primarily to determine whether a customer's business is inside or outside the United States
|
||||
/// for billing purposes.
|
||||
/// </summary>
|
||||
public static class CountryAbbreviations
|
||||
{
|
||||
/// <summary>
|
||||
/// Abbreviation for The United States.
|
||||
/// This value must match what Stripe uses for the `Country` field value for the United States.
|
||||
/// </summary>
|
||||
public const string UnitedStates = "US";
|
||||
}
|
||||
}
|
||||
|
||||
public static class AuthConstants
|
||||
|
||||
@@ -13,5 +13,5 @@ public class TaxInfo
|
||||
public string BillingAddressCity { get; set; }
|
||||
public string BillingAddressState { get; set; }
|
||||
public string BillingAddressPostalCode { get; set; }
|
||||
public string BillingAddressCountry { get; set; } = "US";
|
||||
public string BillingAddressCountry { get; set; } = Constants.CountryAbbreviations.UnitedStates;
|
||||
}
|
||||
|
||||
@@ -9,11 +9,9 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Models;
|
||||
using Bit.Core.Billing.Organizations.Models;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Tax.Models;
|
||||
using Bit.Core.Billing.Tax.Requests;
|
||||
using Bit.Core.Billing.Tax.Responses;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Billing.Tax.Services.Implementations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@@ -21,7 +19,6 @@ 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;
|
||||
@@ -41,8 +38,6 @@ public class StripePaymentService : IPaymentService
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ITaxService _taxService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IAutomaticTaxFactory _automaticTaxFactory;
|
||||
private readonly IAutomaticTaxStrategy _personalUseTaxStrategy;
|
||||
|
||||
public StripePaymentService(
|
||||
ITransactionRepository transactionRepository,
|
||||
@@ -52,9 +47,7 @@ public class StripePaymentService : IPaymentService
|
||||
IGlobalSettings globalSettings,
|
||||
IFeatureService featureService,
|
||||
ITaxService taxService,
|
||||
IPricingClient pricingClient,
|
||||
IAutomaticTaxFactory automaticTaxFactory,
|
||||
[FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy)
|
||||
IPricingClient pricingClient)
|
||||
{
|
||||
_transactionRepository = transactionRepository;
|
||||
_logger = logger;
|
||||
@@ -64,8 +57,6 @@ public class StripePaymentService : IPaymentService
|
||||
_featureService = featureService;
|
||||
_taxService = taxService;
|
||||
_pricingClient = pricingClient;
|
||||
_automaticTaxFactory = automaticTaxFactory;
|
||||
_personalUseTaxStrategy = personalUseTaxStrategy;
|
||||
}
|
||||
|
||||
private async Task ChangeOrganizationSponsorship(
|
||||
@@ -137,7 +128,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (sub.Customer is
|
||||
{
|
||||
Address.Country: not "US",
|
||||
Address.Country: not Constants.CountryAbbreviations.UnitedStates,
|
||||
TaxExempt: not StripeConstants.TaxExempt.Reverse
|
||||
})
|
||||
{
|
||||
@@ -987,8 +978,6 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
_personalUseTaxStrategy.SetInvoiceCreatePreviewOptions(options);
|
||||
|
||||
try
|
||||
{
|
||||
var invoice = await _stripeAdapter.InvoiceCreatePreviewAsync(options);
|
||||
@@ -1152,9 +1141,12 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
var automaticTaxFactoryParameters = new AutomaticTaxFactoryParameters(parameters.PasswordManager.Plan);
|
||||
var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxFactoryParameters);
|
||||
automaticTaxStrategy.SetInvoiceCreatePreviewOptions(options);
|
||||
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
if (parameters.PasswordManager.Plan.IsBusinessProductTierType() &&
|
||||
parameters.TaxInformation.Country != Constants.CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
options.CustomerDetails.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user