1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

[PM-24350] fix tax calculation (#6251)

This commit is contained in:
Kyle Denney
2025-09-03 10:03:49 -05:00
committed by GitHub
parent fa8d65cc1f
commit ef8c7f656d
26 changed files with 663 additions and 1172 deletions

View File

@@ -3,6 +3,7 @@
using System.Globalization;
using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
@@ -282,7 +283,7 @@ public class ProviderBillingService(
]
};
if (providerCustomer.Address is not { Country: "US" })
if (providerCustomer.Address is not { Country: Constants.CountryAbbreviations.UnitedStates })
{
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
}
@@ -525,7 +526,7 @@ public class ProviderBillingService(
}
};
if (taxInfo.BillingAddressCountry is not "US")
if (taxInfo.BillingAddressCountry is not Constants.CountryAbbreviations.UnitedStates)
{
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
}

View File

@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -139,7 +140,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
new string[] { nameof(BillingAddressCountry) });
}
if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
if (PlanType != PlanType.Free && BillingAddressCountry == Constants.CountryAbbreviations.UnitedStates &&
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",

View File

@@ -2,6 +2,7 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
using Bit.Core;
using Bit.Core.Settings;
using Enums = Bit.Core.Enums;
@@ -35,7 +36,7 @@ public class PremiumRequestModel : IValidatableObject
{
yield return new ValidationResult("Payment token or license is required.");
}
if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode))
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(PostalCode) });

View File

@@ -2,6 +2,7 @@
#nullable disable
using System.ComponentModel.DataAnnotations;
using Bit.Core;
namespace Bit.Api.Models.Request.Accounts;
@@ -13,7 +14,7 @@ public class TaxInfoUpdateRequestModel : IValidatableObject
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode))
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(PostalCode) });

View File

@@ -218,7 +218,7 @@ public class StripeEventService : IStripeEventService
private static string GetCustomerRegion(IDictionary<string, string> customerMetadata)
{
const string defaultRegion = "US";
const string defaultRegion = Core.Constants.CountryAbbreviations.UnitedStates;
if (customerMetadata is null)
{

View File

@@ -203,7 +203,7 @@ public class UpcomingInvoiceHandler(
{
var nonUSBusinessUse =
organization.PlanType.GetProductTier() != ProductTierType.Families &&
subscription.Customer.Address.Country != "US";
subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
{
@@ -248,7 +248,7 @@ public class UpcomingInvoiceHandler(
Subscription subscription,
string eventId)
{
if (subscription.Customer.Address.Country != "US" &&
if (subscription.Customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse)
{
try

View File

@@ -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
{

View File

@@ -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>();

View File

@@ -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
{

View File

@@ -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
});

View File

@@ -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
};

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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
{

View File

@@ -181,7 +181,7 @@ public class PreviewTaxAmountCommandTests
options.SubscriptionDetails.Items.Count == 1 &&
options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId &&
options.SubscriptionDetails.Items[0].Quantity == 1 &&
options.AutomaticTax.Enabled == false
options.AutomaticTax.Enabled == true
))
.Returns(expectedInvoice);
@@ -273,4 +273,269 @@ public class PreviewTaxAmountCommandTests
var badRequest = result.AsT1;
Assert.Equal("We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support for assistance.", badRequest.Response);
}
[Fact]
public async Task Run_USBased_PersonalUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_BusinessUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_PersonalUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_USBased_BusinessUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "US",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_PersonalUse_DoesNotSetTaxExempt()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.FamiliesAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
Assert.True(result.IsT0);
}
[Fact]
public async Task Run_NonUSBased_BusinessUse_SetsTaxExemptReverse()
{
// Arrange
var parameters = new OrganizationTrialParameters
{
PlanType = PlanType.EnterpriseAnnually,
ProductType = ProductType.PasswordManager,
TaxInformation = new TaxInformationDTO
{
Country = "CA",
PostalCode = "12345"
}
};
var plan = StaticStore.GetPlan(parameters.PlanType);
_pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan);
var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents
_stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(expectedInvoice);
// Act
var result = await _command.Run(parameters);
// Assert
await _stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
));
Assert.True(result.IsT0);
}
}

View File

@@ -1,105 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Entities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class AutomaticTaxFactoryTests
{
[BitAutoData]
[Theory]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsUser(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(new User(), []);
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[BitAutoData]
[Theory]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenSubscriberIsOrganizationWithFamiliesAnnuallyPrice(
SutProvider<AutomaticTaxFactory> sut)
{
var familiesPlan = new FamiliesPlan();
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [familiesPlan.PasswordManager.StripePlanId]);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(new FamiliesPlan());
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
.Returns(new Families2019Plan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenSubscriberIsOrganizationWithBusinessUsePrice(
EnterpriseAnnually plan,
SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(new Organization(), [plan.PasswordManager.StripePlanId]);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(new FamiliesPlan());
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually2019))
.Returns(new Families2019Plan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsPersonalUseStrategy_WhenPlanIsMeantForPersonalUse(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(PlanType.FamiliesAnnually);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
.Returns(new FamiliesPlan());
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<PersonalUseAutomaticTaxStrategy>(actual);
}
[Theory]
[BitAutoData]
public async Task CreateAsync_ReturnsBusinessUseStrategy_WhenPlanIsMeantForBusinessUse(SutProvider<AutomaticTaxFactory> sut)
{
var parameters = new AutomaticTaxFactoryParameters(PlanType.EnterpriseAnnually);
sut.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == parameters.PlanType.Value))
.Returns(new EnterprisePlan(true));
var actual = await sut.Sut.CreateAsync(parameters);
Assert.IsType<BusinessUseAutomaticTaxStrategy>(actual);
}
public record EnterpriseAnnually : EnterprisePlan
{
public EnterpriseAnnually() : base(true)
{
}
}
}

View File

@@ -1,492 +0,0 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class BusinessUseAutomaticTaxStrategyTests
{
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = null
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.GetUpdateOptions(subscription));
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsNothing_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
Customer = new Customer
{
Address = new()
{
Country = "US"
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.Null(options.AutomaticTax);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsNothing_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.Null(options.AutomaticTax);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.False(options.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForAmericanCustomers(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.True(options.AutomaticTax!.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.True(options.AutomaticTax!.Enabled);
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_ThrowsArgumentNullException_WhenTaxIdsIsNull(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = null
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
Assert.Throws<ArgumentNullException>(() => sutProvider.Sut.SetUpdateOptions(options, subscription));
}
[Theory]
[BitAutoData]
public void SetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
SutProvider<BusinessUseAutomaticTaxStrategy> sutProvider)
{
var options = new SubscriptionUpdateOptions();
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "ES",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
sutProvider.Sut.SetUpdateOptions(options, subscription);
Assert.False(options.AutomaticTax!.Enabled);
}
}

View File

@@ -1,35 +0,0 @@
using Bit.Core.Billing.Tax.Services;
using Stripe;
namespace Bit.Core.Test.Billing.Tax.Services;
/// <param name="isAutomaticTaxEnabled">
/// Whether the subscription options will have automatic tax enabled or not.
/// </param>
public class FakeAutomaticTaxStrategy(
bool isAutomaticTaxEnabled) : IAutomaticTaxStrategy
{
public SubscriptionUpdateOptions? GetUpdateOptions(Subscription subscription)
{
return new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled }
};
}
public void SetCreateOptions(SubscriptionCreateOptions options, Customer customer)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
public void SetUpdateOptions(SubscriptionUpdateOptions options, Subscription subscription)
{
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
public void SetInvoiceCreatePreviewOptions(InvoiceCreatePreviewOptions options)
{
options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = isAutomaticTaxEnabled };
}
}

View File

@@ -1,217 +0,0 @@
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Tax.Services.Implementations;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Stripe;
using Xunit;
namespace Bit.Core.Test.Billing.Tax.Services;
[SutProviderCustomize]
public class PersonalUseAutomaticTaxStrategyTests
{
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenFeatureFlagAllowingToUpdateSubscriptionsIsDisabled(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription();
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(false);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_ReturnsNull_WhenSubscriptionDoesNotNeedUpdating(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Address = new Address
{
Country = "US",
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.Null(actual);
}
[Theory]
[BitAutoData]
public void GetUpdateOptions_SetsAutomaticTaxToFalse_WhenTaxLocationIsUnrecognizedOrInvalid(
SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = true
},
Customer = new Customer
{
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.UnrecognizedLocation
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.False(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForAllCountries(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithTaxIds(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country,
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>
{
new()
{
Country = "ES",
Type = "eu_vat",
Value = "ESZ8880999Z"
}
}
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
[Theory]
[BitAutoData("CA")]
[BitAutoData("ES")]
[BitAutoData("US")]
public void GetUpdateOptions_SetsAutomaticTaxToTrue_ForGlobalCustomersWithoutTaxIds(
string country, SutProvider<PersonalUseAutomaticTaxStrategy> sutProvider)
{
var subscription = new Subscription
{
AutomaticTax = new SubscriptionAutomaticTax
{
Enabled = false
},
Customer = new Customer
{
Address = new Address
{
Country = country
},
Tax = new CustomerTax
{
AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported
},
TaxIds = new StripeList<TaxId>
{
Data = new List<TaxId>()
}
}
};
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(Arg.Is<string>(p => p == FeatureFlagKeys.PM19422_AllowAutomaticTaxUpdates))
.Returns(true);
var actual = sutProvider.Sut.GetUpdateOptions(subscription);
Assert.NotNull(actual);
Assert.True(actual.AutomaticTax.Enabled);
}
}

View File

@@ -1,12 +1,10 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models.StaticStore.Plans;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Requests;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums;
using Bit.Core.Services;
using Bit.Core.Test.Billing.Tax.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@@ -23,10 +21,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -74,10 +68,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -125,10 +115,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithoutAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -177,10 +163,6 @@ public class StripePaymentServiceTests
public async Task PreviewInvoiceAsync_ForOrganization_CalculatesSalesTaxCorrectlyForFamiliesForEnterpriseWithAdditionalStorage(
SutProvider<StripePaymentService> sutProvider)
{
sutProvider.GetDependency<IAutomaticTaxFactory>()
.CreateAsync(Arg.Is<AutomaticTaxFactoryParameters>(p => p.PlanType == PlanType.FamiliesAnnually))
.Returns(new FakeAutomaticTaxStrategy(true));
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
@@ -223,4 +205,340 @@ public class StripePaymentServiceTests
Assert.Equal(4.08M, actual.TotalAmount);
Assert.Equal(4M, actual.TaxableBaseAmount);
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsAutomaticTaxEnabled(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.AutomaticTax.Enabled == true
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_USBased_BusinessUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "US",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_PersonalUse_DoesNotSetTaxExempt(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var familiesPlan = new FamiliesPlan();
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.FamiliesAnnually))
.Returns(familiesPlan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.FamiliesAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == null
));
}
[Theory]
[BitAutoData]
public async Task PreviewInvoiceAsync_NonUSBased_BusinessUse_SetsTaxExemptReverse(SutProvider<StripePaymentService> sutProvider)
{
// Arrange
var plan = new EnterprisePlan(true);
sutProvider.GetDependency<IPricingClient>()
.GetPlanOrThrow(Arg.Is<PlanType>(p => p == PlanType.EnterpriseAnnually))
.Returns(plan);
var parameters = new PreviewOrganizationInvoiceRequestBody
{
PasswordManager = new OrganizationPasswordManagerRequestModel
{
Plan = PlanType.EnterpriseAnnually
},
TaxInformation = new TaxInformationRequestModel
{
Country = "FR",
PostalCode = "12345"
}
};
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
stripeAdapter
.InvoiceCreatePreviewAsync(Arg.Any<InvoiceCreatePreviewOptions>())
.Returns(new Invoice
{
TotalExcludingTax = 400,
Tax = 8,
Total = 408
});
// Act
await sutProvider.Sut.PreviewInvoiceAsync(parameters, null, null);
// Assert
await stripeAdapter.Received(1).InvoiceCreatePreviewAsync(Arg.Is<InvoiceCreatePreviewOptions>(options =>
options.CustomerDetails.TaxExempt == StripeConstants.TaxExempt.Reverse
));
}
}