Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
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 Stripe;
using Stripe.Tax;

namespace Bit.Commercial.Core.Billing.Providers.Queries;

using static Bit.Core.Constants;
using static StripeConstants;
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
Expand Down Expand Up @@ -61,7 +61,7 @@ await subscriberService.GetSubscription(provider,
Provider provider,
Customer customer)
{
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country))
{
return null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,6 +18,7 @@
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;
Expand All @@ -34,7 +34,6 @@

namespace Bit.Commercial.Core.Billing.Providers.Services;

using static Constants;
using static StripeConstants;

public class ProviderBillingService(
Expand Down Expand Up @@ -267,10 +266,13 @@ await subscriberService.GetCustomerOrThrow(provider,
]
};

if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates })
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(providerCustomer.Address?.Country, providerCustomer.TaxExempt);
customerCreateOptions.TaxExempt = providerCustomer switch
{
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
}
{ Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus } when
determinedTaxExemptStatus != customerTaxExemptStatus => determinedTaxExemptStatus,
_ => providerCustomer.TaxExempt
};

var customer = await stripeAdapter.CreateCustomerAsync(customerCreateOptions);

Expand Down Expand Up @@ -467,6 +469,7 @@ public async Task<Customer> SetupCustomer(
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(billingAddress.Country);
var options = new CustomerCreateOptions
{
Address = new AddressOptions
Expand Down Expand Up @@ -494,7 +497,7 @@ public async Task<Customer> SetupCustomer(
]
},
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
TaxExempt = determinedTaxExemptStatus
};

if (billingAddress.TaxId != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,39 @@ public async Task Run_CombinesBothWarningTypes(
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
}

[Theory, BitAutoData]
public async Task Run_SwissCustomer_NoTaxIdWarning(
Provider provider,
SutProvider<GetProviderWarningsQuery> sutProvider)
{
provider.Enabled = true;

sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = SubscriptionStatus.Active,
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "CH" }
}
});

sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().ListTaxRegistrationsAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,55 @@ await sutProvider.GetDependency<IOrganizationRepository>().Received(1).ReplaceAs
org => org.GatewayCustomerId == "customer_id"));
}

[Theory, BitAutoData]
public async Task CreateCustomer_ForClientOrg_USCustomer_SetsTaxExemptToNone(
Provider provider,
Organization organization,
SutProvider<ProviderBillingService> 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<TaxId>
{
Data =
[
new TaxId { Type = "TYPE", Value = "VALUE" }
]
},
TaxExempt = null
};

sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider, Arg.Is<CustomerGetOptions>(
options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids")))
.Returns(providerCustomer);

sutProvider.GetDependency<IGlobalSettings>().BaseServiceUri
.Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings())
{
CloudRegion = "US"
});

sutProvider.GetDependency<IStripeAdapter>().CreateCustomerAsync(Arg.Any<CustomerCreateOptions>())
.Returns(new Customer { Id = "customer_id" });

await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization);

await sutProvider.GetDependency<IStripeAdapter>().Received(1).CreateCustomerAsync(
Arg.Is<CustomerCreateOptions>(options => options.TaxExempt == StripeConstants.TaxExempt.None));
}

#endregion

#region GenerateClientInvoiceReport
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Tax.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Business;
Expand Down Expand Up @@ -139,7 +139,7 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
new string[] { nameof(BillingAddressCountry) });
}

if (PlanType != PlanType.Free && BillingAddressCountry == Constants.CountryAbbreviations.UnitedStates &&
if (PlanType != PlanType.Free && TaxHelpers.IsDirectTaxCountry(BillingAddressCountry) &&
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
Expand Down
4 changes: 2 additions & 2 deletions src/Api/Models/Request/Accounts/PremiumRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#nullable disable

using System.ComponentModel.DataAnnotations;
using Bit.Core;
using Bit.Core.Billing.Tax.Utilities;
using Bit.Core.Settings;
using Enums = Bit.Core.Enums;

Expand Down Expand Up @@ -36,7 +36,7 @@ public IEnumerable<ValidationResult> Validate(ValidationContext validationContex
{
yield return new ValidationResult("Payment token or license is required.");
}
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
if (TaxHelpers.IsDirectTaxCountry(Country) && string.IsNullOrWhiteSpace(PostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(PostalCode) });
Expand Down
4 changes: 2 additions & 2 deletions src/Api/Models/Request/Accounts/TaxInfoUpdateRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
#nullable disable

using System.ComponentModel.DataAnnotations;
using Bit.Core;
using Bit.Core.Billing.Tax.Utilities;

namespace Bit.Api.Models.Request.Accounts;

Expand All @@ -14,7 +14,7 @@ public class TaxInfoUpdateRequestModel : IValidatableObject

public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
if (TaxHelpers.IsDirectTaxCountry(Country) && string.IsNullOrWhiteSpace(PostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
new string[] { nameof(PostalCode) });
Expand Down
69 changes: 39 additions & 30 deletions src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Tax.Utilities;
using Bit.Core.Entities;
using Bit.Core.Models.Mail.Billing.Renewal.Families2019Renewal;
using Bit.Core.Models.Mail.Billing.Renewal.Families2020Renewal;
Expand Down Expand Up @@ -157,24 +158,29 @@ private async Task AlignOrganizationTaxConcernsAsync(
Customer customer,
string eventId)
{
var nonUSBusinessUse =
organization.PlanType.GetProductTier() != ProductTierType.Families &&
customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates;
var isBusinessUse = organization.PlanType.GetProductTier() != ProductTierType.Families;

if (nonUSBusinessUse && customer.TaxExempt != TaxExempt.Reverse)
if (isBusinessUse)
{
try
{
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
}
catch (Exception exception)
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address.Country, customer.TaxExempt);
switch (customer)
{
logger.LogError(
exception,
"Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}",
organization.Id,
eventId);
case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus }
when determinedTaxExemptStatus != customerTaxExemptStatus:
try
{
await stripeFacade.UpdateCustomer(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;
}
}

Expand Down Expand Up @@ -449,22 +455,25 @@ private async Task AlignProviderTaxConcernsAsync(
Customer customer,
string eventId)
{
if (customer.Address.Country != Core.Constants.CountryAbbreviations.UnitedStates &&
customer.TaxExempt != TaxExempt.Reverse)
var determinedTaxExemptStatus = TaxHelpers.DetermineTaxExemptStatus(customer.Address.Country, customer.TaxExempt);
switch (customer)
{
try
{
await stripeFacade.UpdateCustomer(subscription.CustomerId,
new CustomerUpdateOptions { TaxExempt = TaxExempt.Reverse });
}
catch (Exception exception)
{
logger.LogError(
exception,
"Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}",
provider.Id,
eventId);
}
case { Address.Country: not null and not "", TaxExempt: var customerTaxExemptStatus }
when determinedTaxExemptStatus != customerTaxExemptStatus:
try
{
await stripeFacade.UpdateCustomer(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
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 Microsoft.Extensions.Logging;
Expand All @@ -16,7 +17,6 @@

namespace Bit.Core.Billing.Organizations.Commands;

using static Core.Constants;
using static StripeConstants;

public interface IPreviewOrganizationTaxCommand
Expand Down Expand Up @@ -385,12 +385,24 @@ private static InvoiceCreatePreviewOptions GetBaseOptions(
CustomerDetails = new InvoiceCustomerDetailsOptions
{
Address = new AddressOptions { Country = country, PostalCode = postalCode },
TaxExempt = businessUse && country != CountryAbbreviations.UnitedStates
? TaxExempt.Reverse
: TaxExempt.None
}
};

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 =>
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Utilities;
using Bit.Core.Context;
using Stripe;
using Stripe.Tax;

namespace Bit.Core.Billing.Organizations.Queries;

using static Core.Constants;
using static StripeConstants;
using FreeTrialWarning = OrganizationWarnings.FreeTrialWarning;
using InactiveSubscriptionWarning = OrganizationWarnings.InactiveSubscriptionWarning;
Expand Down Expand Up @@ -230,7 +230,7 @@ on the subscription status. */
Customer customer,
Provider? provider)
{
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
if (TaxHelpers.IsDirectTaxCountry(customer.Address?.Country))
{
return null;
}
Expand Down
Loading
Loading