diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs index bf48d3768202..add72b3fc0c1 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Queries/GetProviderWarningsQuery.cs @@ -1,12 +1,9 @@ -using Bit.Core; -using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Providers.Queries; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Context; -using Bit.Core.Services; using Stripe; using Stripe.Tax; @@ -19,7 +16,6 @@ namespace Bit.Commercial.Core.Billing.Providers.Queries; public class GetProviderWarningsQuery( ICurrentContext currentContext, - IFeatureService featureService, IStripeAdapter stripeAdapter, ISubscriberService subscriberService) : IGetProviderWarningsQuery { @@ -65,24 +61,14 @@ await subscriberService.GetSubscription(provider, Provider provider, Customer customer) { - if (featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) + if (customer.TaxExempt != TaxExempt.None) { - if (customer.TaxExempt != TaxExempt.None) - { - return null; - } - - if (customer.Address?.Country == CountryAbbreviations.UnitedStates) - { - return null; - } + return null; } - else + + if (customer.Address?.Country == CountryAbbreviations.UnitedStates) { - if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country)) - { - return null; - } + return null; } if (!currentContext.ProviderProviderAdmin(provider.Id)) diff --git a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs index 4b3e194b5c06..8324be44631f 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -3,7 +3,6 @@ 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; @@ -19,7 +18,6 @@ using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -40,7 +38,6 @@ namespace Bit.Commercial.Core.Billing.Providers.Services; public class ProviderBillingService( IBraintreeGateway braintreeGateway, IEventService eventService, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, @@ -275,17 +272,6 @@ await subscriberService.GetCustomerOrThrow(provider, ] }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(providerCustomer.Address?.Country, providerCustomer.TaxExempt); - customerCreateOptions.TaxExempt = providerCustomer switch - { - { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } when - determinedTaxExemptStatus != customerTaxExemptStatus => determinedTaxExemptStatus, - _ => providerCustomer.TaxExempt - }; - } - var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions); organization.GatewayCustomerId = customer.Id; @@ -511,11 +497,6 @@ public async Task SetupCustomer( Metadata = new Dictionary { { "region", globalSettings.BaseServiceUri.CloudRegion } } }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - options.TaxExempt = TaxHelpers.DetermineTaxExemptStatus(billingAddress.Country); - } - if (billingAddress.TaxId != null) { options.TaxIdData = diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs index 29eb0d4f4912..5d87fb315ba9 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Queries/GetProviderWarningsQueryTests.cs @@ -1,10 +1,8 @@ using Bit.Commercial.Core.Billing.Providers.Queries; -using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Services; using Bit.Core.Context; -using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -59,7 +57,8 @@ public async Task Run_ProviderEnabled_NoSuspensionWarning( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -91,7 +90,8 @@ public async Task Run_Has_SuspensionWarning_AddPaymentMethod( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -125,7 +125,8 @@ public async Task Run_Has_SuspensionWarning_ContactAdministrator( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -159,7 +160,8 @@ public async Task Run_Has_SuspensionWarning_ContactSupport( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -192,7 +194,8 @@ public async Task Run_NotProviderAdmin_NoTaxIdWarning( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -220,7 +223,8 @@ public async Task Run_NoTaxRegistrationForCountry_NoTaxIdWarning( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -253,7 +257,8 @@ public async Task Run_Has_TaxIdMissingWarning( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -292,7 +297,8 @@ public async Task Run_TaxIdVerificationIsNull_NoTaxIdWarning( { Data = [new TaxId { Verification = null }] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -334,7 +340,8 @@ public async Task Run_Has_TaxIdPendingVerificationWarning( } }] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -379,7 +386,8 @@ public async Task Run_Has_TaxIdFailedVerificationWarning( } }] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -424,7 +432,8 @@ public async Task Run_TaxIdVerified_NoTaxIdWarning( } }] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -457,7 +466,8 @@ public async Task Run_MultipleRegistrations_MatchesCorrectCountry( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "DE" } + Address = new Address { Country = "DE" }, + TaxExempt = TaxExempt.None } }); @@ -501,7 +511,8 @@ public async Task Run_CombinesBothWarningTypes( Customer = new Customer { TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CA" } + Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None } }); @@ -522,72 +533,6 @@ public async Task Run_CombinesBothWarningTypes( Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt); } - [Theory, BitAutoData] - public async Task Run_SwissCustomer_NoTaxIdWarning( - Provider provider, - SutProvider sutProvider) - { - provider.Enabled = true; - - sutProvider.GetDependency() - .GetSubscription(provider, Arg.Is(options => - options.Expand.SequenceEqual(_requiredExpansions) - )) - .Returns(new Subscription - { - Status = SubscriptionStatus.Active, - Customer = new Customer - { - TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "CH" } - } - }); - - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); - sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) - .Returns(new StripeList - { - Data = [new Registration { Country = "CH" }] - }); - - var response = await sutProvider.Sut.Run(provider); - - Assert.Null(response!.TaxId); - } - - [Theory, BitAutoData] - public async Task Run_USCustomer_NoTaxIdWarning( - Provider provider, - SutProvider sutProvider) - { - provider.Enabled = true; - - sutProvider.GetDependency() - .GetSubscription(provider, Arg.Is(options => - options.Expand.SequenceEqual(_requiredExpansions) - )) - .Returns(new Subscription - { - Status = SubscriptionStatus.Active, - Customer = new Customer - { - TaxIds = new StripeList { Data = [] }, - Address = new Address { Country = "US" } - } - }); - - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); - sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) - .Returns(new StripeList - { - Data = [new Registration { Country = "US" }] - }); - - var response = await sutProvider.Sut.Run(provider); - - Assert.Null(response!.TaxId); - } - [Theory, BitAutoData] public async Task Run_FlagEnabled_USCustomer_NoTaxIdWarning( Provider provider, @@ -610,10 +555,6 @@ public async Task Run_FlagEnabled_USCustomer_NoTaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var response = await sutProvider.Sut.Run(provider); Assert.Null(response!.TaxId); @@ -641,10 +582,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) .Returns(new StripeList @@ -682,10 +619,6 @@ public async Task Run_FlagEnabled_ExemptCustomer_NoTaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); var response = await sutProvider.Sut.Run(provider); @@ -715,10 +648,6 @@ public async Task Run_FlagEnabled_ReverseCustomer_NoTaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); var response = await sutProvider.Sut.Run(provider); @@ -748,10 +677,6 @@ public async Task Run_FlagEnabled_NoRegistrationInCountry_NoTaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) .Returns(new StripeList @@ -795,10 +720,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdPendingVerificationWa } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) .Returns(new StripeList @@ -845,10 +766,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdFailedVerificationWar } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) .Returns(new StripeList @@ -895,10 +812,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_VerifiedTaxId_NoTaxIdWarning( } }); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency().ProviderProviderAdmin(provider.Id).Returns(true); sutProvider.GetDependency().ListTaxRegistrationsAsync(Arg.Any()) .Returns(new StripeList diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs index 303c28aa9a11..8c89c5a2477a 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/Services/ProviderBillingServiceTests.cs @@ -1,7 +1,6 @@ using System.Globalization; using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Commercial.Core.Billing.Providers.Services; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -310,138 +309,7 @@ await sutProvider.GetDependency().Received(1).ReplaceAs } [Theory, BitAutoData] - public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.GatewayCustomerId = null; - organization.Name = "Name"; - organization.BusinessName = "BusinessName"; - - var providerCustomer = new Customer - { - Address = new Address - { - Country = "CA", - PostalCode = "12345", - Line1 = "123 Main St.", - Line2 = "Unit 4", - City = "Fake Town", - State = "Fake State" - }, - TaxIds = new StripeList - { - Data = - [ - new TaxId { Type = "TYPE", Value = "VALUE" } - ] - } - }; - - sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( - options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) - .Returns(providerCustomer); - - sutProvider.GetDependency().BaseServiceUri - .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) - { - CloudRegion = "US" - }); - - sutProvider.GetDependency().CreateCustomerAsync(Arg.Is( - options => - options.Address.Country == providerCustomer.Address.Country && - options.Address.PostalCode == providerCustomer.Address.PostalCode && - options.Address.Line1 == providerCustomer.Address.Line1 && - options.Address.Line2 == providerCustomer.Address.Line2 && - options.Address.City == providerCustomer.Address.City && - options.Address.State == providerCustomer.Address.State && - options.Name == organization.DisplayName() && - options.Description == $"{provider.Name} Client Organization" && - options.Email == provider.BillingEmail && - options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && - options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && - options.Metadata["region"] == "US" && - options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && - options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value && - options.TaxExempt == StripeConstants.TaxExempt.Reverse)) - .Returns(new Customer { Id = "customer_id" }); - - await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - - await sutProvider.GetDependency().Received(1).CreateCustomerAsync(Arg.Is( - options => - options.Address.Country == providerCustomer.Address.Country && - options.Address.PostalCode == providerCustomer.Address.PostalCode && - options.Address.Line1 == providerCustomer.Address.Line1 && - options.Address.Line2 == providerCustomer.Address.Line2 && - options.Address.City == providerCustomer.Address.City && - options.Address.State == providerCustomer.Address.State && - options.Name == organization.DisplayName() && - options.Description == $"{provider.Name} Client Organization" && - options.Email == provider.BillingEmail && - options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && - options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && - options.Metadata["region"] == "US" && - options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && - options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)); - - await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( - org => org.GatewayCustomerId == "customer_id")); - } - - [Theory, BitAutoData] - public async Task CreateCustomer_ForClientOrg_USCustomer_SetsTaxExemptToNone( - Provider provider, - Organization organization, - SutProvider sutProvider) - { - organization.GatewayCustomerId = null; - organization.Name = "Name"; - - var providerCustomer = new Customer - { - Address = new Address - { - Country = "US", - PostalCode = "12345", - Line1 = "123 Main St.", - Line2 = "Unit 4", - City = "Fake Town", - State = "Fake State" - }, - TaxIds = new StripeList - { - Data = - [ - new TaxId { Type = "TYPE", Value = "VALUE" } - ] - }, - TaxExempt = null - }; - - sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( - options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) - .Returns(providerCustomer); - - sutProvider.GetDependency().BaseServiceUri - .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) - { - CloudRegion = "US" - }); - - sutProvider.GetDependency().CreateCustomerAsync(Arg.Any()) - .Returns(new Customer { Id = "customer_id" }); - - await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); - - await sutProvider.GetDependency().Received(1).CreateCustomerAsync( - Arg.Is(options => options.TaxExempt == StripeConstants.TaxExempt.None)); - } - - [Theory, BitAutoData] - public async Task CreateCustomerForClientOrganization_FlagOn_DoesNotSetTaxExempt( + public async Task CreateCustomerForClientOrganization_DoesNotSetTaxExempt( Provider provider, Organization organization, SutProvider sutProvider) @@ -449,10 +317,6 @@ public async Task CreateCustomerForClientOrganization_FlagOn_DoesNotSetTaxExempt organization.GatewayCustomerId = null; organization.Name = "Name"; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var providerCustomer = new Customer { Address = new Address { Country = "DE", PostalCode = "10115" }, @@ -1208,49 +1072,6 @@ public async Task SetupCustomer_WithCard_Success( Assert.Equivalent(expected, actual); } - [Theory, BitAutoData] - public async Task SetupCustomer_WithCard_ReverseCharge_Success( - SutProvider sutProvider, - Provider provider, - BillingAddress billingAddress) - { - provider.Name = "MSP"; - billingAddress.Country = "FR"; // Non-US country to trigger reverse charge - billingAddress.TaxId = new TaxID("fr_siren", "123456789"); - - var stripeAdapter = sutProvider.GetDependency(); - - var expected = new Customer - { - Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } - }; - - var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" }; - - stripeAdapter.CreateCustomerAsync(Arg.Is(o => - o.Address.Country == billingAddress.Country && - o.Address.PostalCode == billingAddress.PostalCode && - o.Address.Line1 == billingAddress.Line1 && - o.Address.Line2 == billingAddress.Line2 && - o.Address.City == billingAddress.City && - o.Address.State == billingAddress.State && - o.Description == provider.DisplayBusinessName() && - o.Email == provider.BillingEmail && - o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token && - o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() && - o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() && - o.Metadata["region"] == "" && - o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code && - o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value && - o.TaxExempt == StripeConstants.TaxExempt.Reverse)) - .Returns(expected); - - var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress); - - Assert.Equivalent(expected, actual); - } - [Theory, BitAutoData] public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException( SutProvider sutProvider, @@ -1274,7 +1095,7 @@ public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException( } [Theory, BitAutoData] - public async Task SetupCustomer_FlagOn_DoesNotSetTaxExempt( + public async Task SetupCustomer_DoesNotSetTaxExempt( SutProvider sutProvider, Provider provider, BillingAddress billingAddress) @@ -1283,10 +1104,6 @@ public async Task SetupCustomer_FlagOn_DoesNotSetTaxExempt( billingAddress.Country = "FR"; billingAddress.TaxId = null; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" }; var expected = new Customer { Id = "customer_id" }; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 92a74e81d6b5..eedffe1c8548 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -4,11 +4,11 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Utilities; +using CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations; namespace Bit.Api.AdminConsole.Models.Request.Organizations; @@ -147,12 +147,24 @@ public IEnumerable Validate(ValidationContext validationContex new string[] { nameof(BillingAddressCountry) }); } - if (PlanType != PlanType.Free && TaxHelpers.IsDirectTaxCountry(BillingAddressCountry) && + if (PlanType != PlanType.Free && RequiresBillingPostalCode(BillingAddressCountry) && string.IsNullOrWhiteSpace(BillingAddressPostalCode)) { yield return new ValidationResult("Zip / postal code is required.", new string[] { nameof(BillingAddressPostalCode) }); } } + + /// + /// Countries where a billing postal code is required for paid organization signups. + /// + private static readonly HashSet CountriesRequiringBillingPostalCode = + [ + CountryAbbreviations.UnitedStates, + CountryAbbreviations.Switzerland + ]; + + private static bool RequiresBillingPostalCode(string country) => + country is not null and not "" && CountriesRequiringBillingPostalCode.Contains(country); } diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 0973dc8f27ef..3993ee17224d 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -14,7 +14,6 @@ using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Models.Mail.Billing.Renewal.BusinessPlanRenewal2020Migration; using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal; @@ -176,34 +175,6 @@ private async Task AlignOrganizationTaxConcernsAsync( Customer customer, string eventId) { - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - var isBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families; - if (isBusinessUse) - { - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt); - switch (customer) - { - case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } - when determinedTaxExemptStatus != customerTaxExemptStatus: - try - { - await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, - new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus }); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to set organization's ({OrganizationID}) to the required tax exemption while processing event with ID {EventID}", - organization.Id, - eventId); - } - break; - } - } - } - if (!subscription.AutomaticTax.Enabled) { try @@ -845,30 +816,6 @@ private async Task AlignProviderTaxConcernsAsync( Customer customer, string eventId) { - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt); - switch (customer) - { - case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } - when determinedTaxExemptStatus != customerTaxExemptStatus: - try - { - await stripeAdapter.UpdateCustomerAsync(subscription.CustomerId, - new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus }); - } - catch (Exception exception) - { - logger.LogError( - exception, - "Failed to set provider's ({ProviderID}) to the required tax exemption while processing event with ID {EventID}", - provider.Id, - eventId); - } - break; - } - } - if (!subscription.AutomaticTax.Enabled) { try diff --git a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs index 747b993cce3a..f4f0b9404e67 100644 --- a/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs +++ b/src/Core/Billing/Organizations/Commands/PreviewOrganizationTaxCommand.cs @@ -8,10 +8,8 @@ using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Services; using Microsoft.Extensions.Logging; using OneOf; using Stripe; @@ -38,7 +36,6 @@ public interface IPreviewOrganizationTaxCommand } public class PreviewOrganizationTaxCommand( - IFeatureService featureService, ILogger logger, IPricingClient pricingClient, IStripeAdapter stripeAdapter, @@ -398,24 +395,6 @@ private InvoiceCreatePreviewOptions GetBaseOptions( } }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - switch (businessUse) - { - case true: - var existingTaxExemptStatus = addressChoice.Match( - customer => customer.TaxExempt, - _ => null!); - - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(country, existingTaxExemptStatus); - options.CustomerDetails.TaxExempt = determinedTaxExemptStatus; - break; - default: - options.CustomerDetails.TaxExempt = TaxExempt.None; - break; - } - } - var taxId = addressChoice.Match( customer => { diff --git a/src/Core/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommand.cs b/src/Core/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommand.cs index 285dfe34a48e..82ee17e868d3 100644 --- a/src/Core/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommand.cs +++ b/src/Core/Billing/Organizations/Commands/UpdateOrganizationSubscriptionCommand.cs @@ -8,8 +8,6 @@ using Bit.Core.Billing.Organizations.PlanMigration.ValueObjects; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; -using Bit.Core.Services; using Microsoft.Extensions.Logging; using OneOf; using Stripe; @@ -47,7 +45,6 @@ Task> Run( } public class UpdateOrganizationSubscriptionCommand( - IFeatureService featureService, ILogger logger, IOrganizationPlanMigrationCohortAssignmentRepository assignmentRepository, IOrganizationPlanMigrationCohortRepository cohortRepository, @@ -94,11 +91,6 @@ public Task> Run( return new Conflict("No changes were provided for the organization subscription update"); } - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - await ReconcileTaxExemptionAsync(subscription.Customer); - } - var hasStructuralChanges = changeSet.ChargeImmediately; var isChargedAutomatically = subscription.CollectionMethod == CollectionMethod.ChargeAutomatically; var isBilledAnnually = subscription.Items.FirstOrDefault()?.Price.Recurring?.Interval == Intervals.Year; @@ -267,20 +259,6 @@ private static bool HasRequiredExpansions(Subscription? subscription) => return MigrationPaths.FromId(cohort.MigrationPathId.Value); } - private async Task ReconcileTaxExemptionAsync(Customer customer) - { - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt); - switch (customer) - { - case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } - when determinedTaxExemptStatus != customerTaxExemptStatus: - await stripeAdapter.UpdateCustomerAsync(customer.Id, - new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus }); - break; - } - - } - private static OneOf ValidateItemAddition( AddItem addItem, Subscription subscription) { diff --git a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs index afd6b4c812da..848f5a1cde63 100644 --- a/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs +++ b/src/Core/Billing/Organizations/Queries/GetOrganizationWarningsQuery.cs @@ -11,7 +11,6 @@ using Bit.Core.Billing.Payment.Queries; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Context; using Bit.Core.Services; using Stripe; @@ -253,24 +252,14 @@ on the subscription status. */ Customer customer, Provider? provider) { - if (featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) + if (customer.TaxExempt != TaxExempt.None) { - if (customer.TaxExempt != TaxExempt.None) - { - return null; - } - - if (customer.Address?.Country == CountryAbbreviations.UnitedStates) - { - return null; - } + return null; } - else + + if (customer.Address?.Country == CountryAbbreviations.UnitedStates) { - if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country)) - { - return null; - } + return null; } var productTier = organization.PlanType.GetProductTier(); diff --git a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs index 190cb903b6e5..820fed26ff45 100644 --- a/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs +++ b/src/Core/Billing/Organizations/Services/OrganizationBillingService.cs @@ -7,12 +7,10 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Braintree; using Microsoft.Extensions.Logging; @@ -27,7 +25,6 @@ namespace Bit.Core.Billing.Organizations.Services; public class OrganizationBillingService( IBraintreeGateway braintreeGateway, - IFeatureService featureService, IGlobalSettings globalSettings, IHasPaymentMethodQuery hasPaymentMethodQuery, ILogger logger, @@ -51,7 +48,7 @@ public async Task Finalize(OrganizationSale sale) var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType) - : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup); + : await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); var subscription = await CreateSubscriptionAsync(organization, customer, subscriptionSetup, coupons); @@ -261,15 +258,6 @@ private async Task CreateCustomerAsync( ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - if (planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && - !TaxHelpers.IsDirectTaxCountry(customerSetup.TaxInformation.Country)) - { - customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; - } - } - if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) { var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, @@ -504,35 +492,6 @@ private async Task CreateSubscriptionAsync( return subscription; } - private async Task GetCustomerWhileEnsuringCorrectTaxExemptionAsync( - Organization organization, - SubscriptionSetup subscriptionSetup) - { - var customer = await subscriberService.GetCustomerOrThrow(organization, - new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); - - if (featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) || subscriptionSetup.PlanType.GetProductTier() is - not (ProductTierType.Teams or - ProductTierType.TeamsStarter or - ProductTierType.Enterprise)) - { - return customer; - } - - List expansions = ["tax", "tax_ids"]; - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address?.Country, customer.TaxExempt); - customer = customer switch - { - { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } - when determinedTaxExemptStatus != customerTaxExemptStatus => - await stripeAdapter.UpdateCustomerAsync(customer.Id, - new CustomerUpdateOptions { Expand = expansions, TaxExempt = determinedTaxExemptStatus }), - _ => customer - }; - - return customer; - } - #endregion } diff --git a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs index 852ceb28acb1..baf1daa8cf78 100644 --- a/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs +++ b/src/Core/Billing/Payment/Commands/UpdateBillingAddressCommand.cs @@ -3,7 +3,6 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Payment.Models; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Services; using Microsoft.Extensions.Logging; @@ -86,11 +85,6 @@ private async Task> UpdateBusinessBillingAd Expand = ["subscriptions", "subscriptions.data.test_clock", "tax_ids"] }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - updateOptions.TaxExempt = await GetDeterminedTaxExemptStatusAsync(subscriber.GatewayCustomerId!, billingAddress.Country); - } - var customer = await stripeAdapter.UpdateCustomerAsync(subscriber.GatewayCustomerId, updateOptions); await EnableAutomaticTaxAsync(subscriber, customer); @@ -123,13 +117,6 @@ private async Task> UpdateBusinessBillingAd return BillingAddress.From(customer.Address, updatedTaxId); } - - private async Task GetDeterminedTaxExemptStatusAsync(string customerId, string? billingCountry) - { - var existingCustomer = await stripeAdapter.GetCustomerAsync(customerId); - return TaxHelpers.DetermineTaxExemptStatus(billingCountry, existingCustomer.TaxExempt); - } - private async Task EnableAutomaticTaxAsync( ISubscriber subscriber, Customer customer) diff --git a/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs index 35b37de2ba25..2a609bc1e0ce 100644 --- a/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs +++ b/src/Core/Billing/Premium/Commands/UpgradePremiumToOrganizationCommand.cs @@ -8,7 +8,6 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Subscriptions.Models; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; @@ -66,8 +65,7 @@ public class UpgradePremiumToOrganizationCommand( IBraintreeService braintreeService, IGetPaymentMethodQuery getPaymentMethodQuery, IOrganizationAbilityCacheService organizationAbilityCacheService, - IPushNotificationService pushNotificationService, - IFeatureService featureService) + IPushNotificationService pushNotificationService) : BaseBillingCommand(logger), IUpgradePremiumToOrganizationCommand { private readonly ILogger _logger = logger; @@ -139,11 +137,6 @@ public Task> Run( } }; - if (!featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - addressUpdateOptions.TaxExempt = TaxHelpers.DetermineTaxExemptStatus(billingAddress.Country); - } - var customer = await stripeAdapter.UpdateCustomerAsync(user.GatewayCustomerId, addressUpdateOptions); // Add tax ID to the customer for accurate tax calculation if provided diff --git a/src/Core/Billing/Services/Implementations/StripePaymentService.cs b/src/Core/Billing/Services/Implementations/StripePaymentService.cs index 47ace034bd17..4b5e8405b9ac 100644 --- a/src/Core/Billing/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Billing/Services/Implementations/StripePaymentService.cs @@ -11,14 +11,12 @@ using Bit.Core.Billing.Organizations.PlanMigration.Enums; using Bit.Core.Billing.Organizations.PlanMigration.ValueObjects; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Tax.Utilities; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.BitStripe; using Bit.Core.Models.Business; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; using Stripe; @@ -37,7 +35,6 @@ public class StripePaymentService : IStripePaymentService private readonly IStripeAdapter _stripeAdapter; private readonly IGlobalSettings _globalSettings; private readonly IPricingClient _pricingClient; - private readonly IFeatureService _featureService; public StripePaymentService( ITransactionRepository transactionRepository, @@ -45,8 +42,7 @@ public StripePaymentService( IStripeAdapter stripeAdapter, Braintree.IBraintreeGateway braintreeGateway, IGlobalSettings globalSettings, - IPricingClient pricingClient, - IFeatureService featureService) + IPricingClient pricingClient) { _transactionRepository = transactionRepository; _logger = logger; @@ -54,7 +50,6 @@ public StripePaymentService( _btGateway = braintreeGateway; _globalSettings = globalSettings; _pricingClient = pricingClient; - _featureService = featureService; } // TODO: Remove with FF: pm-32581-use-update-organization-subscription-command -> Updated SetUpSponsorshipCommand @@ -129,19 +124,6 @@ private async Task FinalizeSubscriptionChangeAsync(ISubscriber subscribe if (subscriptionUpdate is CompleteSubscriptionUpdate) { - if (!_featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax)) - { - var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(sub.Customer.Address?.Country, sub.Customer.TaxExempt); - switch (sub.Customer) - { - case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } - when determinedTaxExemptStatus != customerTaxExemptStatus: - await _stripeAdapter.UpdateCustomerAsync(sub.Customer.Id, - new CustomerUpdateOptions { TaxExempt = determinedTaxExemptStatus }); - break; - } - } - subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } diff --git a/src/Core/Billing/Tax/Utilities/TaxHelpers.cs b/src/Core/Billing/Tax/Utilities/TaxHelpers.cs deleted file mode 100644 index 27fc16e8eeb2..000000000000 --- a/src/Core/Billing/Tax/Utilities/TaxHelpers.cs +++ /dev/null @@ -1,36 +0,0 @@ -using CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations; -using TaxExempt = Bit.Core.Billing.Constants.StripeConstants.TaxExempt; -namespace Bit.Core.Billing.Tax.Utilities; - -public static class TaxHelpers -{ - /// - /// Countries where tax is collected directly from customers, rather than through VAT ID reverse charge. - /// To add a new country, add its ISO 3166 code to - /// and then add it to this set. - /// - private static readonly HashSet DirectTaxCountries = - [ - CountryAbbreviations.UnitedStates, - CountryAbbreviations.Switzerland - ]; - - /// - /// Returns if is in , - /// meaning tax is collected directly and Stripe's tax_exempt should default to "none". - /// Returns for all other countries, where VAT reverse charge applies. - /// - public static bool IsDirectTaxCountry(string? country) => - country is not null and not "" && DirectTaxCountries.Contains(country); - - /// - /// Returns the Stripe tax_exempt value appropriate for .
- /// If is already "exempt", that status is always preserved.
- /// For direct-tax countries, returns "none".
- /// For all other countries, returns "reverse". - ///
- public static string DetermineTaxExemptStatus(string? country, string? currentTaxExempt = null) => - currentTaxExempt == TaxExempt.Exempt - ? TaxExempt.Exempt - : IsDirectTaxCountry(country) ? TaxExempt.None : TaxExempt.Reverse; -} diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7fe09339b569..a4ce99d804c9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -192,7 +192,6 @@ public static class FeatureFlagKeys public const string PM34515_BrowserDesktopCheckout = "pm-34515-browser-desktop-checkout"; public const string DebugDisableSelfHostPremiumCheck = "debug-disable-self-host-premium-check"; public const string PM35215_BusinessPlanPriceMigration = "pm-35215-business-plan-price-migration"; - public const string PM37597_AlwaysEnableStripeAutomaticTax = "pm-37597-always-enable-stripe-automatic-tax"; /* Key Management Team */ public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration"; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModelTests.cs index 091488049260..d367bee134c3 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModelTests.cs @@ -1,6 +1,8 @@ using System.ComponentModel.DataAnnotations; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Core.Billing.Enums; using Xunit; +using CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations; namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations; @@ -182,6 +184,44 @@ public void ToOrganizationSignup_TrialLength_IsMapped() Assert.Equal(14, signup.TrialLength); } + [Theory] + [InlineData(CountryAbbreviations.UnitedStates, true)] + [InlineData(CountryAbbreviations.Switzerland, true)] + [InlineData("DE", false)] + [InlineData(null, false)] + [InlineData("", false)] + public void Validate_PaidPlanWithoutPostalCode_RequiresPostalCodeForExpectedCountries( + string? billingAddressCountry, + bool postalCodeRequired) + { + var model = new OrganizationCreateRequestModel + { + Name = "Test Org", + BillingEmail = "test@example.com", + Key = "test-key", + UseSecretsManager = false, + Keys = new OrganizationKeysRequestModel + { + PublicKey = "test-public-key", + EncryptedPrivateKey = "test-encrypted-private-key" + }, + PlanType = PlanType.TeamsAnnually2023, + BillingAddressCountry = billingAddressCountry, + BillingAddressPostalCode = null + }; + + var results = ValidateModel(model); + + if (postalCodeRequired) + { + Assert.Contains(results, r => r.MemberNames.Contains(nameof(OrganizationCreateRequestModel.BillingAddressPostalCode))); + } + else + { + Assert.DoesNotContain(results, r => r.MemberNames.Contains(nameof(OrganizationCreateRequestModel.BillingAddressPostalCode))); + } + } + private static List ValidateModel(object model) { var context = new ValidationContext(model); diff --git a/test/Billing.IntegrationTest/LegacyBillingFlagsFixture.cs b/test/Billing.IntegrationTest/LegacyBillingFlagsFixture.cs index 574431ecc339..0d5561207693 100644 --- a/test/Billing.IntegrationTest/LegacyBillingFlagsFixture.cs +++ b/test/Billing.IntegrationTest/LegacyBillingFlagsFixture.cs @@ -21,9 +21,6 @@ protected override ApiApplicationFactory CreateApi() api.UpdateConfiguration( $"globalSettings:launchDarkly:flagValues:{FeatureFlagKeys.PM32581_UseUpdateOrganizationSubscriptionCommand}", "false"); - api.UpdateConfiguration( - $"globalSettings:launchDarkly:flagValues:{FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax}", - "false"); return api; } diff --git a/test/Billing.IntegrationTest/LegacyBillingFlowsTests.cs b/test/Billing.IntegrationTest/LegacyBillingFlowsTests.cs index 2d5082b67cd2..7f46c0a0c395 100644 --- a/test/Billing.IntegrationTest/LegacyBillingFlowsTests.cs +++ b/test/Billing.IntegrationTest/LegacyBillingFlowsTests.cs @@ -5,8 +5,8 @@ namespace Bit.Billing.IntegrationTest; /// -/// Tests that exercise legacy billing code paths gated by PM32581 / PM37597. -/// They use so the flags resolve to +/// Tests that exercise legacy billing code paths gated by PM32581. +/// They use so the flag resolves to /// false during the host's lifetime. /// public class LegacyBillingFlowsTests(LegacyBillingFlagsFixture fixture) : IClassFixture @@ -36,7 +36,7 @@ public async Task PlanUpgrade_FromFamilies_ReusesCustomerViaFinalize() { // Families org → existing Stripe customer. With PM32581 off the legacy // UpgradeOrganizationPlanCommand path runs and calls OrganizationBillingService - // .Finalize → GetCustomerWhileEnsuringCorrectTaxExemptionAsync. + // .Finalize, which reuses the existing customer via subscriberService.GetCustomerOrThrow. var (client, _, organizationId, _) = await fixture.PrepareOrganizationOwnerAsync( "legacy-plan-upgrade@example.com", PlanType.FamiliesAnnually); @@ -53,34 +53,4 @@ await fixture.PrepareOrganizationOwnerAsync( }); await Assert.SuccessResponseAsync(response); } - - [BillingFact] - public async Task PlanUpgrade_FromFamilies_ReconcilesTaxExemptionForNonUsAddress() - { - // Drive the legacy tax-exemption reconciliation branch: PM37597 must be off AND the - // new plan must be Teams/Enterprise AND customer.TaxExempt must differ from the - // determined value (forced by using a non-US billing address where Bitwarden has no - // tax registration, which flips TaxExempt to Reverse). - var (client, _, organizationId, _) = - await fixture.PrepareOrganizationOwnerAsync( - "legacy-tax-reconcile@example.com", PlanType.FamiliesAnnually); - - // Move the Families org's address out of US so the determined exemption changes. - var addressResponse = await client.PutAsJsonAsync( - $"/organizations/{organizationId}/billing/vnext/address", - new { Country = "BR", PostalCode = "01310-100" }); - await Assert.SuccessResponseAsync(addressResponse); - - var upgradeResponse = await client.PostAsJsonAsync( - $"/organizations/{organizationId}/upgrade", - new - { - PlanType = PlanType.EnterpriseAnnually, - AdditionalSeats = 5, - UseSecretsManager = false, - BillingAddressCountry = "BR", - BillingAddressPostalCode = "01310-100", - }); - await Assert.SuccessResponseAsync(upgradeResponse); - } } diff --git a/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs b/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs index b6add1c6cbbb..a3aa636f7183 100644 --- a/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs +++ b/test/Billing.Test/Services/UpcomingInvoiceHandlerTests.cs @@ -572,147 +572,6 @@ await _mailService.Received(1).SendInvoiceUpcoming( Arg.Is(b => b == true)); } - [Fact] - public async Task HandleAsync_WhenNonDirectTaxCountryOrganization_SetsReverseCharge() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary() - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "DE" }, - TaxExempt = TaxExempt.None - }; - var organization = new Organization - { - Id = _organizationId, - BillingEmail = "org@example.com", - PlanType = PlanType.EnterpriseAnnually - }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(_organizationId, null, null)); - _organizationRepository.GetByIdAsync(_organizationId).Returns(organization); - _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true)); - _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _stripeAdapter.Received(1).UpdateCustomerAsync( - Arg.Is("cus_123"), - Arg.Is(o => o.TaxExempt == TaxExempt.Reverse)); - } - - [Fact] - public async Task HandleAsync_WhenUSOrganizationWithManualReverseCharge_CorrectsTaxExemptToNone() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary() - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "US" }, - TaxExempt = TaxExempt.Reverse - }; - var organization = new Organization - { - Id = _organizationId, - BillingEmail = "org@example.com", - PlanType = PlanType.EnterpriseAnnually - }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(_organizationId, null, null)); - _organizationRepository.GetByIdAsync(_organizationId).Returns(organization); - _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true)); - _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _stripeAdapter.Received(1).UpdateCustomerAsync( - Arg.Is("cus_123"), - Arg.Is(o => o.TaxExempt == TaxExempt.None)); - } - - [Fact] - public async Task HandleAsync_WhenSwissOrganizationWithReverse_CorrectsTaxExemptToNone() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary() - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "CH" }, - TaxExempt = TaxExempt.Reverse - }; - var organization = new Organization - { - Id = _organizationId, - BillingEmail = "org@example.com", - PlanType = PlanType.EnterpriseAnnually - }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(_organizationId, null, null)); - _organizationRepository.GetByIdAsync(_organizationId).Returns(organization); - _pricingClient.GetPlanOrThrow(organization.PlanType).Returns(new EnterprisePlan(isAnnual: true)); - _stripeEventUtilityService.IsSponsoredSubscription(subscription).Returns(false); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _stripeAdapter.Received(1).UpdateCustomerAsync( - "cus_123", - Arg.Is(options => options.TaxExempt == TaxExempt.None)); - } - [Fact] public async Task HandleAsync_WhenOrganizationCustomerIsExempt_DoesNotUpdateTaxExemption() { @@ -812,11 +671,6 @@ public async Task HandleAsync_WhenValidProviderSubscription_SendsEmail() // Assert await _providerRepository.Received(2).GetByIdAsync(_providerId); - // Verify tax exempt was set to reverse for non-direct-tax-country providers - await _stripeAdapter.Received(1).UpdateCustomerAsync( - Arg.Is("cus_123"), - Arg.Is(o => o.TaxExempt == TaxExempt.Reverse)); - // Verify automatic tax was enabled await _stripeAdapter.Received(1).UpdateSubscriptionAsync( Arg.Is("sub_123"), @@ -833,63 +687,6 @@ await _mailService.Received(1).SendProviderInvoiceUpcoming( Arg.Is(s => s == $"{paymentMethod.Brand} ending in {paymentMethod.Last4}")); } - [Fact] - public async Task HandleAsync_WhenSwissProviderWithReverse_CorrectsTaxExemptToNone() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice - { - CustomerId = "cus_123", - AmountDue = 10000, - NextPaymentAttempt = DateTime.UtcNow.AddDays(7), - Lines = new StripeList - { - Data = [new() { Description = "Test Item" }] - } - }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary(), - CollectionMethod = "charge_automatically" - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "CH" }, - TaxExempt = TaxExempt.Reverse - }; - var provider = new Provider { Id = _providerId, BillingEmail = "provider@example.com" }; - - var paymentMethod = new Card { Last4 = "4242", Brand = "visa" }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(null, null, _providerId)); - - _providerRepository.GetByIdAsync(_providerId).Returns(provider); - _getPaymentMethodQuery.Run(provider).Returns(MaskedPaymentMethod.From(paymentMethod)); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _providerRepository.Received(2).GetByIdAsync(_providerId); - - await _stripeAdapter.Received(1).UpdateCustomerAsync( - "cus_123", - Arg.Is(options => options.TaxExempt == TaxExempt.None)); - } - [Fact] public async Task HandleAsync_WhenProviderCustomerIsExempt_DoesNotUpdateTaxExemption() { @@ -942,88 +739,6 @@ await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( Arg.Any()); } - [Fact] - public async Task HandleAsync_WhenNonDirectTaxCountryProvider_SetsReverseCharge() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary(), - CollectionMethod = "charge_automatically" - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "DE" }, - TaxExempt = TaxExempt.None - }; - var provider = new Provider { Id = _providerId, BillingEmail = "provider@example.com" }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(null, null, _providerId)); - _providerRepository.GetByIdAsync(_providerId).Returns(provider); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _stripeAdapter.Received(1).UpdateCustomerAsync( - Arg.Is("cus_123"), - Arg.Is(o => o.TaxExempt == TaxExempt.Reverse)); - } - - [Fact] - public async Task HandleAsync_WhenUSProviderWithManualReverseCharge_CorrectsTaxExemptToNone() - { - // Arrange - var parsedEvent = new Event { Id = "evt_123" }; - var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; - var subscription = new Subscription - { - Id = "sub_123", - CustomerId = "cus_123", - Items = new StripeList(), - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true }, - Customer = new Customer { Id = "cus_123" }, - Metadata = new Dictionary(), - CollectionMethod = "charge_automatically" - }; - var customer = new Customer - { - Id = "cus_123", - Subscriptions = new StripeList { Data = [subscription] }, - Address = new Address { Country = "US" }, - TaxExempt = TaxExempt.Reverse - }; - var provider = new Provider { Id = _providerId, BillingEmail = "provider@example.com" }; - - _stripeEventService.GetInvoice(parsedEvent).Returns(invoice); - _stripeAdapter.GetCustomerAsync(invoice.CustomerId, Arg.Any()).Returns(customer); - _stripeEventUtilityService - .GetIdsFromMetadata(subscription.Metadata) - .Returns(new Tuple(null, null, _providerId)); - _providerRepository.GetByIdAsync(_providerId).Returns(provider); - - // Act - await _sut.HandleAsync(parsedEvent); - - // Assert - await _stripeAdapter.Received(1).UpdateCustomerAsync( - Arg.Is("cus_123"), - Arg.Is(o => o.TaxExempt == TaxExempt.None)); - } - [Fact] public async Task HandleAsync_WhenUpdateSubscriptionItemPriceIdFails_LogsErrorAndSendsTraditionalEmail() { @@ -3365,10 +3080,8 @@ await _stripeAdapter.Received(1).UpdateSubscriptionScheduleAsync( } [Fact] - public async Task HandleAsync_FlagOn_OrganizationWithMismatchedTaxExempt_DoesNotUpdateCustomerTaxExempt() + public async Task HandleAsync_OrganizationWithMismatchedTaxExempt_DoesNotUpdateCustomerTaxExempt() { - _featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax).Returns(true); - var parsedEvent = new Event { Id = "evt_123" }; var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; var subscription = new Subscription @@ -3410,10 +3123,8 @@ await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( } [Fact] - public async Task HandleAsync_FlagOn_ProviderWithMismatchedTaxExempt_DoesNotUpdateCustomerTaxExempt() + public async Task HandleAsync_ProviderWithMismatchedTaxExempt_DoesNotUpdateCustomerTaxExempt() { - _featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax).Returns(true); - var parsedEvent = new Event { Id = "evt_123" }; var invoice = new Invoice { CustomerId = "cus_123", AmountDue = 0, Lines = new StripeList { Data = [] } }; var subscription = new Subscription diff --git a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs index ad5ca2390e81..c3493e606249 100644 --- a/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs +++ b/test/Core.Test/Billing/Organizations/Commands/PreviewOrganizationTaxCommandTests.cs @@ -6,7 +6,6 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Entities; -using Bit.Core.Services; using Bit.Core.Test.Billing.Mocks.Plans; using Microsoft.Extensions.Logging; using NSubstitute; @@ -18,7 +17,6 @@ namespace Bit.Core.Test.Billing.Organizations.Commands; public class PreviewOrganizationTaxCommandTests { - private readonly IFeatureService _featureService = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); private readonly IPricingClient _pricingClient = Substitute.For(); private readonly IStripeAdapter _stripeAdapter = Substitute.For(); @@ -29,7 +27,7 @@ public class PreviewOrganizationTaxCommandTests public PreviewOrganizationTaxCommandTests() { _user = new User { Id = Guid.NewGuid(), Email = "test@example.com" }; - _command = new PreviewOrganizationTaxCommand(_featureService, _logger, _pricingClient, _stripeAdapter, _subscriptionDiscountService); + _command = new PreviewOrganizationTaxCommand(_logger, _pricingClient, _stripeAdapter, _subscriptionDiscountService); } #region Subscription Purchase @@ -79,7 +77,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-teams-org-seat-monthly" && item.Quantity == 5) && @@ -200,7 +196,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is()).Returns(invoice); - - var result = await _command.Run(_user, purchase, billingAddress); - - Assert.True(result.IsT0); - var (tax, total) = result.AsT0; - Assert.Equal(0.00m, tax); - Assert.Equal(27.00m, total); - - // Verify the correct Stripe API call for business use in non-US country (tax exempt reverse) - await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true && - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "DE" && - options.CustomerDetails.Address.PostalCode == "10115" && - options.CustomerDetails.TaxExempt == TaxExempt.Reverse && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && - options.SubscriptionDetails.Items[0].Quantity == 3 && - options.Discounts == null)); - } - - [Fact] - public async Task Run_OrganizationSubscriptionPurchase_BusinessUseSwitzerland_UsesTaxExemptNone() - { - var purchase = new OrganizationSubscriptionPurchase - { - Tier = ProductTierType.Teams, - Cadence = PlanCadenceType.Monthly, - PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections - { - Seats = 3, - AdditionalStorage = 0, - Sponsored = false - } - }; - - var billingAddress = new BillingAddress - { - Country = "CH", - PostalCode = "3001" - }; - - var plan = new TeamsPlan(false); - _pricingClient.GetPlanOrThrow(purchase.PlanType).Returns(plan); - - var invoice = new Invoice - { - TotalTaxes = [new InvoiceTotalTax { Amount = 220 }], - Total = 2920 - }; - - _stripeAdapter.CreateInvoicePreviewAsync(Arg.Any()).Returns(invoice); - - var result = await _command.Run(_user, purchase, billingAddress); - - Assert.True(result.IsT0); - var (tax, total) = result.AsT0; - Assert.Equal(2.20m, tax); - Assert.Equal(29.20m, total); - - await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is(options => - options.AutomaticTax.Enabled == true && - options.Currency == "usd" && - options.CustomerDetails.Address.Country == "CH" && - options.CustomerDetails.Address.PostalCode == "3001" && - options.CustomerDetails.TaxExempt == TaxExempt.None && - options.SubscriptionDetails.Items.Count == 1 && - options.SubscriptionDetails.Items[0].Price == "2023-teams-org-seat-monthly" && - options.SubscriptionDetails.Items[0].Quantity == 3 && - options.Discounts == null)); - } - [Fact] public async Task Run_OrganizationSubscriptionPurchase_SpanishNIFTaxId_AddsEUVATTaxId() { @@ -417,7 +308,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is t.Type == TaxIdType.SpanishNIF && t.Value == "12345678Z") && options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == "ES12345678Z") && @@ -473,7 +363,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 10) && @@ -591,7 +479,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-teams-org-seat-monthly" && item.Quantity == 5) && @@ -707,7 +593,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 2) && @@ -1424,7 +1298,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 8) && @@ -1565,7 +1438,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2023-enterprise-org-seat-annually" && item.Quantity == 15) && @@ -1802,7 +1672,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is t.Type == TaxIdType.SpanishNIF && t.Value == "12345678Z") && options.CustomerDetails.TaxIds.Any(t => t.Type == TaxIdType.EUVAT && t.Value == "ES12345678Z") && @@ -2027,7 +1894,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is item.Price == "2020-families-org-annually" && item.Quantity == 6) && @@ -2129,7 +1995,6 @@ await _stripeAdapter.Received(1).CreateInvoicePreviewAsync(Arg.Is(); private readonly IStripeAdapter _stripeAdapter = Substitute.For(); private readonly IPricingClient _pricingClient = Substitute.For(); private readonly IOrganizationPlanMigrationCohortAssignmentRepository _assignmentRepository = @@ -43,7 +41,6 @@ public UpdateOrganizationSubscriptionCommandTests() .Returns(MockPlans.Get(PlanType.EnterpriseAnnually2020)); _command = new UpdateOrganizationSubscriptionCommand( - _featureService, Substitute.For>(), _assignmentRepository, _cohortRepository, @@ -748,7 +745,7 @@ public async Task Run_SendInvoice_NotChargeImmediately_DoesNotProcessInvoice() } [Fact] - public async Task Run_NonUSCustomer_NotReverseExempt_UpdatesTaxExemption() + public async Task Run_MismatchedTaxExempt_DoesNotReconcile() { var customer = new Customer { @@ -763,198 +760,6 @@ public async Task Run_NonUSCustomer_NotReverseExempt_UpdatesTaxExemption() SetupGetSubscription(organization, subscription); SetupUpdateSubscription(subscription); - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id, - Arg.Is(options => - options.TaxExempt == TaxExempt.Reverse)); - } - - [Fact] - public async Task Run_NonUSCustomer_AlreadyReverseExempt_DoesNotUpdateTaxExemption() - { - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = "DE" }, - TaxExempt = TaxExempt.Reverse - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( - Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Run_USCustomer_DoesNotUpdateTaxExemption() - { - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = "US" }, - TaxExempt = TaxExempt.None - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( - Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Run_SwissCustomer_WithNone_DoesNotUpdateTaxExemption() - { - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = "CH" }, - TaxExempt = TaxExempt.None - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( - Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Run_SwissCustomer_WithReverse_UpdatesTaxExemptToNone() - { - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = "CH" }, - TaxExempt = TaxExempt.Reverse - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.Received(1).UpdateCustomerAsync(customer.Id, - Arg.Is(options => - options.TaxExempt == TaxExempt.None)); - } - - [Theory] - [InlineData("CH")] - [InlineData("US")] - [InlineData("DE")] - public async Task Run_CustomerWithExemptStatus_DoesNotUpdateTaxExemption(string country) - { - // "exempt" is a manual designation (e.g. non-profit) and must never be overwritten automatically. - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = country }, - TaxExempt = TaxExempt.Exempt - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( - Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Run_CustomerWithNullAddress_DoesNotUpdateTaxExemption() - { - var customer = new Customer { Id = "cus_123", Address = null }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - - var changeSet = new OrganizationSubscriptionChangeSet - { - Changes = [new UpdateItemQuantity("price_seats", 10)] - }; - - await _command.Run(organization, changeSet); - - await _stripeAdapter.DidNotReceive().UpdateCustomerAsync( - Arg.Any(), Arg.Any()); - } - - [Fact] - public async Task Run_FlagOn_MismatchedTaxExempt_DoesNotReconcile() - { - _featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax).Returns(true); - - var customer = new Customer - { - Id = "cus_123", - Address = new Address { Country = "DE" }, - TaxExempt = TaxExempt.None - }; - - var organization = CreateOrganization(); - var subscription = CreateSubscription(customer: customer, items: [("price_seats", "si_1", 5)]); - - SetupGetSubscription(organization, subscription); - SetupUpdateSubscription(subscription); - await _command.Run(organization, new OrganizationSubscriptionChangeSet { Changes = [new UpdateItemQuantity("price_seats", 10)] diff --git a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs index 074d85e1ed4c..33da3ef67c22 100644 --- a/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs +++ b/test/Core.Test/Billing/Organizations/Queries/GetOrganizationWarningsQueryTests.cs @@ -413,56 +413,6 @@ public async Task Run_Has_ResellerRenewalWarning_PastDue( Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate); } - [Theory, BitAutoData] - public async Task Run_USCustomer_NoTaxIdWarning( - Organization organization, - SutProvider sutProvider) - { - var subscription = new Subscription - { - Customer = new Customer - { - Address = new Address { Country = "US" }, - TaxIds = new StripeList { Data = new List() }, - InvoiceSettings = new CustomerInvoiceSettings(), - Metadata = new Dictionary() - } - }; - - sutProvider.GetDependency() - .GetSubscription(organization, Arg.Any()) - .Returns(subscription); - - var response = await sutProvider.Sut.Run(organization); - - Assert.Null(response.TaxId); - } - - [Theory, BitAutoData] - public async Task Run_CHCustomer_NoTaxIdWarning( - Organization organization, - SutProvider sutProvider) - { - var subscription = new Subscription - { - Customer = new Customer - { - Address = new Address { Country = "CH" }, - TaxIds = new StripeList { Data = new List() }, - InvoiceSettings = new CustomerInvoiceSettings(), - Metadata = new Dictionary() - } - }; - - sutProvider.GetDependency() - .GetSubscription(organization, Arg.Any()) - .Returns(subscription); - - var response = await sutProvider.Sut.Run(organization); - - Assert.Null(response.TaxId); - } - [Theory, BitAutoData] public async Task Run_FreeCustomer_NoTaxIdWarning( Organization organization, @@ -475,6 +425,7 @@ public async Task Run_FreeCustomer_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List() }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -502,6 +453,7 @@ public async Task Run_NotOwner_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List() }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -533,6 +485,7 @@ public async Task Run_HasProvider_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List() }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -568,6 +521,7 @@ public async Task Run_NoRegistrationInCountry_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List() }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -609,6 +563,7 @@ public async Task Run_Has_TaxIdWarning_Missing( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List() }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -661,6 +616,7 @@ public async Task Run_Has_TaxIdWarning_PendingVerification( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List { taxId } }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -713,6 +669,7 @@ public async Task Run_Has_TaxIdWarning_FailedVerification( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List { taxId } }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -765,6 +722,7 @@ public async Task Run_VerifiedTaxId_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List { taxId } }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -811,6 +769,7 @@ public async Task Run_NullVerification_NoTaxIdWarning( Customer = new Customer { Address = new Address { Country = "CA" }, + TaxExempt = TaxExempt.None, TaxIds = new StripeList { Data = new List { taxId } }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary() @@ -888,10 +847,6 @@ public async Task Run_FlagEnabled_USCustomer_NoTaxIdWarning( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var response = await sutProvider.Sut.Run(organization); Assert.Null(response.TaxId); @@ -920,10 +875,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdWarning_Missing( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency() .OrganizationOwner(organization.Id) .Returns(true); @@ -969,10 +920,6 @@ public async Task Run_FlagEnabled_ExemptCustomer_NoTaxIdWarning( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var response = await sutProvider.Sut.Run(organization); Assert.Null(response.TaxId); @@ -1001,10 +948,6 @@ public async Task Run_FlagEnabled_ReverseCustomer_NoTaxIdWarning( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var response = await sutProvider.Sut.Run(organization); Assert.Null(response.TaxId); @@ -1033,10 +976,6 @@ public async Task Run_FlagEnabled_NoRegistrationInCountry_NoTaxIdWarning( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency() .OrganizationOwner(organization.Id) .Returns(true); @@ -1087,10 +1026,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdWarning_PendingVerifi .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency() .OrganizationOwner(organization.Id) .Returns(true); @@ -1144,10 +1079,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_Has_TaxIdWarning_FailedVerific .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency() .OrganizationOwner(organization.Id) .Returns(true); @@ -1201,10 +1132,6 @@ public async Task Run_FlagEnabled_TaxableCustomer_VerifiedTaxId_NoTaxIdWarning( .GetSubscription(organization, Arg.Any()) .Returns(subscription); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - sutProvider.GetDependency() .OrganizationOwner(organization.Id) .Returns(true); diff --git a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs index d9c8b8f350df..2ec35f32dc5b 100644 --- a/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs +++ b/test/Core.Test/Billing/Payment/Commands/UpdateBillingAddressCommandTests.cs @@ -199,8 +199,7 @@ public async Task Run_BusinessOrganization_MakesCorrectInvocations_ReturnsBillin _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.None + options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") )).Returns(customer); var result = await _command.Run(organization, input); @@ -270,8 +269,7 @@ public async Task Run_BusinessOrganization_RemovingTaxId_MakesCorrectInvocations _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.None + options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") )).Returns(customer); var result = await _command.Run(organization, input); @@ -286,132 +284,6 @@ await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySub await _stripeAdapter.Received(1).DeleteTaxIdAsync(customer.Id, "tax_id_123"); } - [Fact] - public async Task Run_NonUSBusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress() - { - var organization = new Organization - { - PlanType = PlanType.EnterpriseAnnually, - GatewayCustomerId = "cus_123", - GatewaySubscriptionId = "sub_123" - }; - - var input = new BillingAddress - { - Country = "DE", - PostalCode = "10115", - Line1 = "Friedrichstraße 123", - Line2 = "Stock 3", - City = "Berlin", - State = "Berlin" - }; - - var customer = new Customer - { - Address = new Address - { - Country = "DE", - PostalCode = "10115", - Line1 = "Friedrichstraße 123", - Line2 = "Stock 3", - City = "Berlin", - State = "Berlin" - }, - Subscriptions = new StripeList - { - Data = - [ - new Subscription - { - Id = organization.GatewaySubscriptionId, - AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } - } - ] - } - }; - - _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId) - .Returns(new Customer { TaxExempt = TaxExempt.None }); - - _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => - options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.Reverse - )).Returns(customer); - - var result = await _command.Run(organization, input); - - Assert.True(result.IsT0); - var output = result.AsT0; - Assert.Equivalent(input, output); - - await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId, - Arg.Is(options => options.AutomaticTax.Enabled == true)); - } - - [Fact] - public async Task Run_SwissBusinessOrganization_MakesCorrectInvocations_ReturnsBillingAddress() - { - var organization = new Organization - { - PlanType = PlanType.EnterpriseAnnually, - GatewayCustomerId = "cus_123", - GatewaySubscriptionId = "sub_123" - }; - - var input = new BillingAddress - { - Country = "CH", - PostalCode = "3001", - Line1 = "Bundesgasse 1", - Line2 = string.Empty, - City = "Bern", - State = "BE" - }; - - var customer = new Customer - { - Address = new Address - { - Country = "CH", - PostalCode = "3001", - Line1 = "Bundesgasse 1", - Line2 = string.Empty, - City = "Bern", - State = "BE" - }, - Subscriptions = new StripeList - { - Data = - [ - new Subscription - { - Id = organization.GatewaySubscriptionId, - AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } - } - ] - } - }; - - _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId) - .Returns(new Customer { TaxExempt = TaxExempt.None }); - - _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => - options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.None - )).Returns(customer); - - var result = await _command.Run(organization, input); - - Assert.True(result.IsT0); - var output = result.AsT0; - Assert.Equivalent(input, output); - - await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySubscriptionId, - Arg.Is(options => options.AutomaticTax.Enabled == true)); - } - [Fact] public async Task Run_BusinessOrganizationWithSpanishCIF_MakesCorrectInvocations_ReturnsBillingAddress() { @@ -463,8 +335,7 @@ public async Task Run_BusinessOrganizationWithSpanishCIF_MakesCorrectInvocations _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.Reverse + options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") )).Returns(customer); _stripeAdapter @@ -543,8 +414,7 @@ public async Task Run_BusinessOrganization_UpdatingWithSameTaxId_DeletesBeforeCr _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.None + options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") )).Returns(customer); var newTaxId = new TaxId { Id = "tax_id_456", Type = "us_ein", Value = "987654321" }; @@ -570,131 +440,6 @@ await _stripeAdapter.Received(1).CreateTaxIdAsync(customer.Id, Arg.Is options.Type == "us_ein" && options.Value == "987654321")); } - [Fact] - public async Task Run_SwissBusinessOrganization_WithReverse_CorrectsTaxExemptToNone() - { - // CH is a direct-tax country — "reverse" is not preserved. A customer moving from a - // non-direct-tax country (where "reverse" was correctly set) to Switzerland should have - // their tax_exempt corrected to "none". - var organization = new Organization - { - PlanType = PlanType.EnterpriseAnnually, - GatewayCustomerId = "cus_123", - GatewaySubscriptionId = "sub_123" - }; - - var input = new BillingAddress - { - Country = "CH", - PostalCode = "3001", - Line1 = "Bundesgasse 1", - Line2 = string.Empty, - City = "Bern", - State = "BE" - }; - - var customer = new Customer - { - Address = new Address - { - Country = "CH", - PostalCode = "3001", - Line1 = "Bundesgasse 1", - Line2 = string.Empty, - City = "Bern", - State = "BE" - }, - Subscriptions = new StripeList - { - Data = - [ - new Subscription - { - Id = organization.GatewaySubscriptionId, - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true } - } - ] - } - }; - - _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId) - .Returns(new Customer { TaxExempt = TaxExempt.Reverse }); - - _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => - options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.None - )).Returns(customer); - - var result = await _command.Run(organization, input); - - Assert.True(result.IsT0); - var output = result.AsT0; - Assert.Equivalent(input, output); - - await _stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId, - Arg.Is(options => options.TaxExempt == TaxExempt.None)); - } - - [Fact] - public async Task Run_BusinessOrganizationWithExemptStatus_PreservesExempt() - { - var organization = new Organization - { - PlanType = PlanType.EnterpriseAnnually, - GatewayCustomerId = "cus_123", - GatewaySubscriptionId = "sub_123" - }; - - var input = new BillingAddress - { - Country = "US", - PostalCode = "12345", - Line1 = "123 Main St.", - City = "New York", - State = "NY" - }; - - var customer = new Customer - { - Address = new Address - { - Country = "US", - PostalCode = "12345", - Line1 = "123 Main St.", - City = "New York", - State = "NY" - }, - Subscriptions = new StripeList - { - Data = - [ - new Subscription - { - Id = organization.GatewaySubscriptionId, - AutomaticTax = new SubscriptionAutomaticTax { Enabled = true } - } - ] - } - }; - - _stripeAdapter.GetCustomerAsync(organization.GatewayCustomerId) - .Returns(new Customer { TaxExempt = TaxExempt.Exempt }); - - _stripeAdapter.UpdateCustomerAsync(organization.GatewayCustomerId, Arg.Is(options => - options.Address.Matches(input) && - options.HasExpansions("subscriptions", "subscriptions.data.test_clock", "tax_ids") && - options.TaxExempt == TaxExempt.Exempt - )).Returns(customer); - - var result = await _command.Run(organization, input); - - Assert.True(result.IsT0); - - await _stripeAdapter.Received(1).UpdateCustomerAsync(organization.GatewayCustomerId, - Arg.Is(options => options.TaxExempt == TaxExempt.Exempt)); - } - [Fact] public async Task Run_PersonalOrganization_FlagOn_SchedulePresent_UpdatesSchedulePhasesAndDefaultSettings() { @@ -1050,10 +795,8 @@ await _stripeAdapter.Received(1).UpdateSubscriptionAsync(organization.GatewaySub } [Fact] - public async Task Run_FlagOn_BusinessOrganization_DoesNotSetTaxExempt() + public async Task Run_BusinessOrganization_DoesNotSetTaxExempt() { - _featureService.IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax).Returns(true); - var organization = new Organization { PlanType = PlanType.EnterpriseAnnually, diff --git a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs index 3a18b6cbab4c..1b30be96c3cf 100644 --- a/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs +++ b/test/Core.Test/Billing/Premium/Commands/UpgradePremiumToOrganizationCommandTests.cs @@ -148,7 +148,6 @@ private static List CreateTestPremiumPlansList() private readonly IGetPaymentMethodQuery _getPaymentMethodQuery = Substitute.For(); private readonly IPushNotificationService _pushNotificationService = Substitute.For(); private readonly ILogger _logger = Substitute.For>(); - private readonly IFeatureService _featureService = Substitute.For(); private readonly UpgradePremiumToOrganizationCommand _command; public UpgradePremiumToOrganizationCommandTests() @@ -180,8 +179,7 @@ public UpgradePremiumToOrganizationCommandTests() _braintreeService, _getPaymentMethodQuery, _organizationAbilityCacheService, - _pushNotificationService, - _featureService); + _pushNotificationService); } private static Core.Billing.Payment.Models.BillingAddress CreateTestBillingAddress() => @@ -1200,7 +1198,7 @@ await _collectionRepository.Received(1).CreateAsync( } [Theory, BitAutoData] - public async Task Run_WithNoTaxId_SetsTaxExemptToNone_DoesNotCreateTaxId(User user) + public async Task Run_WithNoTaxId_DoesNotCreateTaxId(User user) { // Arrange user.Premium = true; @@ -1246,15 +1244,11 @@ public async Task Run_WithNoTaxId_SetsTaxExemptToNone_DoesNotCreateTaxId(User us // Assert Assert.True(result.Success); - await _stripeAdapter.Received(1).UpdateCustomerAsync( - "cus_123", - Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)); await _stripeAdapter.DidNotReceive().CreateTaxIdAsync(Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task Run_WithTaxId_SetsTaxExemptToReverse_CreatesOneTaxId(User user) + public async Task Run_WithTaxId_CreatesOneTaxId(User user) { // Arrange user.Premium = true; @@ -1301,10 +1295,6 @@ public async Task Run_WithTaxId_SetsTaxExemptToReverse_CreatesOneTaxId(User user // Assert Assert.True(result.Success); - await _stripeAdapter.Received(1).UpdateCustomerAsync( - "cus_123", - Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.Reverse)); await _stripeAdapter.Received(1).CreateTaxIdAsync( "cus_123", Arg.Is(options => @@ -1379,7 +1369,7 @@ await _braintreeService.Received(1).PayInvoice( } [Theory, BitAutoData] - public async Task Run_WithSpanishNIF_SetsTaxExemptToReverse_CreatesBothSpanishNIFAndEUVAT(User user) + public async Task Run_WithSpanishNIF_CreatesBothSpanishNIFAndEUVAT(User user) { // Arrange user.Premium = true; @@ -1427,11 +1417,6 @@ public async Task Run_WithSpanishNIF_SetsTaxExemptToReverse_CreatesBothSpanishNI // Assert Assert.True(result.Success); - await _stripeAdapter.Received(1).UpdateCustomerAsync( - "cus_123", - Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.Reverse)); - // Verify Spanish NIF was created await _stripeAdapter.Received(1).CreateTaxIdAsync( "cus_123", @@ -1448,7 +1433,7 @@ await _stripeAdapter.Received(1).CreateTaxIdAsync( } [Theory, BitAutoData] - public async Task Run_WithSwissCountry_SetsTaxExemptToNone(User user) + public async Task Run_DoesNotSetTaxExempt(User user) { user.Premium = true; user.GatewaySubscriptionId = "sub_123"; @@ -1498,7 +1483,7 @@ public async Task Run_WithSwissCountry_SetsTaxExemptToNone(User user) await _stripeAdapter.Received(1).UpdateCustomerAsync( "cus_123", Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)); + options.TaxExempt == null)); await _stripeAdapter.DidNotReceive().CreateTaxIdAsync(Arg.Any(), Arg.Any()); } diff --git a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs index bb5f97d342a8..a9461020efec 100644 --- a/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs +++ b/test/Core.Test/Billing/Services/OrganizationBillingServiceTests.cs @@ -10,7 +10,6 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Test.Billing.Mocks; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -764,73 +763,6 @@ await sutProvider.GetDependency() .CreateSubscriptionAsync(Arg.Any()); } - [Theory, BitAutoData] - public async Task Finalize_BusinessWithExemptStatus_DoesNotUpdateTaxExemption( - Organization organization, - SutProvider sutProvider) - { - // Arrange - var plan = MockPlans.Get(PlanType.TeamsAnnually); - organization.PlanType = PlanType.TeamsAnnually; - organization.GatewayCustomerId = "cus_test123"; - organization.GatewaySubscriptionId = null; - - var subscriptionSetup = new SubscriptionSetup - { - PlanType = PlanType.TeamsAnnually, - PasswordManagerOptions = new SubscriptionSetup.PasswordManager - { - Seats = 5, - Storage = null, - PremiumAccess = false - }, - SecretsManagerOptions = null, - SkipTrial = false - }; - - var sale = new OrganizationSale - { - Organization = organization, - SubscriptionSetup = subscriptionSetup - }; - - var customer = new Customer - { - Id = "cus_test123", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }, - Address = new Address { Country = "DE" }, - TaxExempt = StripeConstants.TaxExempt.Exempt - }; - - sutProvider.GetDependency() - .GetPlanOrThrow(PlanType.TeamsAnnually) - .Returns(plan); - - sutProvider.GetDependency() - .GetCustomerOrThrow(organization, Arg.Any()) - .Returns(customer); - - sutProvider.GetDependency() - .CreateSubscriptionAsync(Arg.Any()) - .Returns(new Subscription - { - Id = "sub_test123", - Status = StripeConstants.SubscriptionStatus.Active - }); - - sutProvider.GetDependency() - .ReplaceAsync(organization) - .Returns(Task.CompletedTask); - - // Act - await sutProvider.Sut.Finalize(sale); - - // Assert - await sutProvider.GetDependency() - .DidNotReceive() - .UpdateCustomerAsync(Arg.Any(), Arg.Any()); - } - [Theory, BitAutoData] public async Task Finalize_WithSMTrialSystemCoupon_AppliesSmStandaloneToSubscription( Organization organization, @@ -1105,169 +1037,6 @@ await sutProvider.GetDependency() #endregion - [Theory, BitAutoData] - public async Task Finalize_SwissBusinessWithReverse_CorrectsTaxExemptToNone( - Organization organization, - SutProvider sutProvider) - { - // Arrange - var plan = MockPlans.Get(PlanType.TeamsAnnually); - organization.PlanType = PlanType.TeamsAnnually; - organization.GatewayCustomerId = "cus_test123"; - organization.GatewaySubscriptionId = null; - - var subscriptionSetup = new SubscriptionSetup - { - PlanType = PlanType.TeamsAnnually, - PasswordManagerOptions = new SubscriptionSetup.PasswordManager - { - Seats = 5, - Storage = null, - PremiumAccess = false - }, - SecretsManagerOptions = null, - SkipTrial = false - }; - - var sale = new OrganizationSale - { - Organization = organization, - SubscriptionSetup = subscriptionSetup - }; - - var customer = new Customer - { - Id = "cus_test123", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }, - Address = new Address { Country = "CH" }, - TaxExempt = StripeConstants.TaxExempt.Reverse - }; - - var correctedCustomer = new Customer - { - Id = "cus_test123", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }, - Address = new Address { Country = "CH" }, - TaxExempt = StripeConstants.TaxExempt.None - }; - - sutProvider.GetDependency() - .GetPlanOrThrow(PlanType.TeamsAnnually) - .Returns(plan); - - sutProvider.GetDependency() - .GetCustomerOrThrow(organization, Arg.Any()) - .Returns(customer); - - sutProvider.GetDependency() - .UpdateCustomerAsync(customer.Id, Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)) - .Returns(correctedCustomer); - - sutProvider.GetDependency() - .CreateSubscriptionAsync(Arg.Any()) - .Returns(new Subscription - { - Id = "sub_test123", - Status = StripeConstants.SubscriptionStatus.Active - }); - - sutProvider.GetDependency() - .ReplaceAsync(organization) - .Returns(Task.CompletedTask); - - // Act - await sutProvider.Sut.Finalize(sale); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .UpdateCustomerAsync("cus_test123", - Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)); - } - - [Theory, BitAutoData] - public async Task Finalize_USBusinessWithReverseExempt_CorrectsTaxExemptToNone( - Organization organization, - SutProvider sutProvider) - { - // Arrange - var plan = MockPlans.Get(PlanType.TeamsAnnually); - organization.PlanType = PlanType.TeamsAnnually; - organization.GatewayCustomerId = "cus_test123"; - organization.GatewaySubscriptionId = null; - - var subscriptionSetup = new SubscriptionSetup - { - PlanType = PlanType.TeamsAnnually, - PasswordManagerOptions = new SubscriptionSetup.PasswordManager - { - Seats = 5, - Storage = null, - PremiumAccess = false - }, - SecretsManagerOptions = null, - SkipTrial = false - }; - - var sale = new OrganizationSale - { - Organization = organization, - SubscriptionSetup = subscriptionSetup - }; - - var customer = new Customer - { - Id = "cus_test123", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }, - Address = new Address { Country = "US" }, - TaxExempt = StripeConstants.TaxExempt.Reverse - }; - - var correctedCustomer = new Customer - { - Id = "cus_test123", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }, - Address = new Address { Country = "US" }, - TaxExempt = StripeConstants.TaxExempt.None - }; - - sutProvider.GetDependency() - .GetPlanOrThrow(PlanType.TeamsAnnually) - .Returns(plan); - - sutProvider.GetDependency() - .GetCustomerOrThrow(organization, Arg.Any()) - .Returns(customer); - - sutProvider.GetDependency() - .UpdateCustomerAsync(customer.Id, Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)) - .Returns(correctedCustomer); - - sutProvider.GetDependency() - .CreateSubscriptionAsync(Arg.Any()) - .Returns(new Subscription - { - Id = "sub_test123", - Status = StripeConstants.SubscriptionStatus.Active - }); - - sutProvider.GetDependency() - .ReplaceAsync(organization) - .Returns(Task.CompletedTask); - - // Act - await sutProvider.Sut.Finalize(sale); - - // Assert: UpdateCustomerAsync called with TaxExempt = "none" to correct the erroneous "reverse" - await sutProvider.GetDependency() - .Received(1) - .UpdateCustomerAsync(customer.Id, Arg.Is(options => - options.TaxExempt == StripeConstants.TaxExempt.None)); - } - [Theory, BitAutoData] public async Task UpdateOrganizationNameAndEmail_UpdatesStripeCustomer( Organization organization, @@ -1438,7 +1207,7 @@ await stripeAdapter.Received(1).UpdateCustomerAsync( } [Theory, BitAutoData] - public async Task Finalize_FlagOn_ExistingMismatchedTaxExempt_DoesNotReconcile( + public async Task Finalize_ExistingMismatchedTaxExempt_DoesNotReconcile( Organization organization, SutProvider sutProvider) { @@ -1446,10 +1215,6 @@ public async Task Finalize_FlagOn_ExistingMismatchedTaxExempt_DoesNotReconcile( organization.GatewayCustomerId = "cus_test123"; organization.GatewaySubscriptionId = null; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.PM37597_AlwaysEnableStripeAutomaticTax) - .Returns(true); - var plan = MockPlans.Get(PlanType.TeamsAnnually); sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsAnnually).Returns(plan); diff --git a/test/Core.Test/Billing/Tax/TaxHelpersTests.cs b/test/Core.Test/Billing/Tax/TaxHelpersTests.cs deleted file mode 100644 index 1fd6c4947ad3..000000000000 --- a/test/Core.Test/Billing/Tax/TaxHelpersTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -using Bit.Core.Billing.Tax.Utilities; -using Xunit; -using CountryAbbreviations = Bit.Core.Constants.CountryAbbreviations; -using TaxExempt = Bit.Core.Billing.Constants.StripeConstants.TaxExempt; - -namespace Bit.Core.Test.Billing.Tax; - -public class TaxHelpersTests -{ - - [Theory] - [InlineData(CountryAbbreviations.UnitedStates, true)] - [InlineData(CountryAbbreviations.Switzerland, true)] - [InlineData("DE", false)] - [InlineData(null, false)] - [InlineData("", false)] - public void IsDirectTaxCountry_ReturnsExpectedResult(string? country, bool expected) - { - var result = TaxHelpers.IsDirectTaxCountry(country); - Assert.Equal(expected, result); - } - - [Theory] - [InlineData("DE", TaxExempt.None, TaxExempt.Reverse)] // non-direct-tax → Reverse - [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.Reverse, TaxExempt.None)] // US Reverse → None (direct-tax) - [InlineData(CountryAbbreviations.Switzerland, null, TaxExempt.None)] // CH no existing status → None - [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.None, TaxExempt.None)] // US already None → None - [InlineData(CountryAbbreviations.Switzerland, TaxExempt.Reverse, TaxExempt.None)] // CH Reverse → None (direct-tax, not preserved) - [InlineData("DE", TaxExempt.Reverse, TaxExempt.Reverse)] // non-direct-tax already Reverse → Reverse - [InlineData(null, TaxExempt.None, TaxExempt.Reverse)] // unknown country → Reverse - [InlineData("DE", TaxExempt.Exempt, TaxExempt.Exempt)] // exempt always preserved — non-direct-tax country - [InlineData(CountryAbbreviations.UnitedStates, TaxExempt.Exempt, TaxExempt.Exempt)] // exempt always preserved — direct-tax country - [InlineData(CountryAbbreviations.Switzerland, TaxExempt.Exempt, TaxExempt.Exempt)] // exempt always preserved — CH - public void DetermineTaxExemptStatus_ReturnsExpectedResult( - string? country, - string? currentTaxExempt, - string expected) - { - var result = TaxHelpers.DetermineTaxExemptStatus(country, currentTaxExempt); - Assert.Equal(expected, result); - } -}