Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-19147] Automatic Tax Improvements #5510

Closed
wants to merge 9 commits into from
Closed
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 @@ -28,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient;
private readonly IOrganizationAutomaticTaxStrategy _organizationAutomaticTaxStrategy;

public RemoveOrganizationFromProviderCommand(
IEventService eventService,
Expand All @@ -40,7 +41,8 @@ public RemoveOrganizationFromProviderCommand(
IProviderBillingService providerBillingService,
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
IPricingClient pricingClient,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
{
_eventService = eventService;
_mailService = mailService;
Expand All @@ -53,6 +55,7 @@ public RemoveOrganizationFromProviderCommand(
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
_organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy;
}

public async Task RemoveOrganizationFromProvider(
Expand Down Expand Up @@ -107,7 +110,7 @@ private async Task ResetOrganizationBillingAsync(
organization.IsValidClient() &&
!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{
Description = string.Empty,
Email = organization.BillingEmail
Expand All @@ -120,7 +123,6 @@ private async Task ResetOrganizationBillingAsync(
Customer = organization.GatewayCustomerId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary<string, string>
{
{ "organizationId", organization.Id.ToString() }
Expand All @@ -130,6 +132,8 @@ private async Task ResetOrganizationBillingAsync(
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
};

await _organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);

var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);

organization.GatewaySubscriptionId = subscription.Id;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public class ProviderBillingService(
IProviderUserRepository providerUserRepository,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
ITaxService taxService) : IProviderBillingService
ITaxService taxService,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy) : IProviderBillingService
{
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task AddExistingOrganization(
Expand Down Expand Up @@ -589,10 +590,6 @@ public async Task<Subscription> SetupSubscription(

var subscriptionCreateOptions = new SubscriptionCreateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
},
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Customer = customer.Id,
DaysUntilDue = 30,
Expand All @@ -605,6 +602,8 @@ public async Task<Subscription> SetupSubscription(
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
};

await organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ It looks strange to me to be calling something named for organizations for providers. My suggestion is that we align your new tax strategies with our actual domain use cases:

  • PersonalUseTaxStrategy: Premium users and family organizations
  • BusinessUseTaxStrategy: Non-family organizations and providers


try
{
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,26 @@ public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_Conso
Id = "subscription_id"
});

sutProvider.GetDependency<IOrganizationAutomaticTaxStrategy>()
.When(x => x.SetCreateOptionsAsync(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == organization.GatewayCustomerId &&
options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice &&
options.DaysUntilDue == 30 &&
options.Metadata["organizationId"] == organization.Id.ToString() &&
options.OffSession == true &&
options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations &&
options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId &&
options.Items.First().Quantity == organization.Seats)
, Arg.Any<Customer>()))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});

await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization);

await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(options =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -975,11 +975,12 @@ public async Task SetupSubscription_Succeeds(
{
provider.GatewaySubscriptionId = null;

sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider).Returns(new Customer
var customer = new Customer
{
Id = "customer_id",
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
});
};
sutProvider.GetDependency<ISubscriberService>().GetCustomerOrThrow(provider).Returns(customer);

var providerPlans = new List<ProviderPlan>
{
Expand Down Expand Up @@ -1017,6 +1018,19 @@ public async Task SetupSubscription_Succeeds(

var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active };

sutProvider.GetDependency<IOrganizationAutomaticTaxStrategy>()
.When(x => x.SetCreateOptionsAsync(
Arg.Is<SubscriptionCreateOptions>(options =>
options.Customer == "customer_id")
, Arg.Is<Customer>(p => p == customer)))
.Do(x =>
{
x.Arg<SubscriptionCreateOptions>().AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
};
});

sutProvider.GetDependency<IStripeAdapter>().SubscriptionCreateAsync(Arg.Is<SubscriptionCreateOptions>(
sub =>
sub.AutomaticTax.Enabled == true &&
Expand Down
37 changes: 10 additions & 27 deletions src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand All @@ -21,7 +20,9 @@
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand)
IValidateSponsorshipCommand validateSponsorshipCommand,
IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)

Check warning on line 25 in src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs#L23-L25

Added lines #L23 - L25 were not covered by tests
: IUpcomingInvoiceHandler
{
public async Task HandleAsync(Event parsedEvent)
Expand Down Expand Up @@ -136,33 +137,15 @@

private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
{
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
var updateOptions = subscription.IsOrganization()
? await organizationAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription)
: individualAutomaticTaxStrategy.GetUpdateOptions(subscription);

Check warning on line 142 in src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs#L141-L142

Added lines #L141 - L142 were not covered by tests

if (updateOptions == null)
{
return;
}

await stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
{
DefaultTaxRates = [],
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});

return;

async Task<bool> IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription)
{
var familyPriceIds = (await Task.WhenAll(
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);

return localSubscription.Customer.Address.Country != "US" &&
localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) &&
!localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
!localSubscription.Customer.TaxIds.Any();
}
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);

Check warning on line 149 in src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs

View check run for this annotation

Codecov / codecov/patch

src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs#L149

Added line #L149 was not covered by tests
}
}
2 changes: 2 additions & 0 deletions src/Core/Billing/Constants/StripeConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ public static class InvoiceStatus
public static class MetadataKeys
{
public const string OrganizationId = "organizationId";
public const string ProviderId = "providerId";
public const string UserId = "userId";
}

public static class PaymentBehavior
Expand Down
12 changes: 1 addition & 11 deletions src/Core/Billing/Extensions/CustomerExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,13 @@ namespace Bit.Core.Billing.Extensions;

public static class CustomerExtensions
{
public static bool HasBillingLocation(this Customer customer)
=> customer is
{
Address:
{
Country: not null and not "",
PostalCode: not null and not ""
}
};

/// <summary>
/// Determines if a Stripe customer supports automatic tax
/// </summary>
/// <param name="customer"></param>
/// <returns></returns>
public static bool HasTaxLocationVerified(this Customer customer) =>
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation;

public static decimal GetBillingBalance(this Customer customer)
{
Expand Down
3 changes: 3 additions & 0 deletions src/Core/Billing/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Services.Implementations;
using Bit.Core.Billing.Services.Implementations.AutomaticTax;

namespace Bit.Core.Billing.Extensions;

Expand All @@ -18,6 +19,8 @@ public static void AddBillingOperations(this IServiceCollection services)
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
services.AddTransient<ISubscriberService, SubscriberService>();
services.AddTransient<IIndividualAutomaticTaxStrategy, IndividualAutomaticTaxStrategy>();
services.AddTransient<IOrganizationAutomaticTaxStrategy, OrganizationAutomaticTaxStrategy>();
services.AddLicenseServices();
services.AddPricingClient();
}
Expand Down

This file was deleted.

12 changes: 12 additions & 0 deletions src/Core/Billing/Extensions/SubscriptionExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Bit.Core.Billing.Constants;
using Stripe;

namespace Bit.Core.Billing.Extensions;

public static class SubscriptionExtensions
{
public static bool IsOrganization(this Subscription subscription)
{
return subscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId);
}

Check warning on line 11 in src/Core/Billing/Extensions/SubscriptionExtensions.cs

View check run for this annotation

Codecov / codecov/patch

src/Core/Billing/Extensions/SubscriptionExtensions.cs#L9-L11

Added lines #L9 - L11 were not covered by tests
}

This file was deleted.

Loading
Loading