1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-25 23:02:17 -05:00
This commit is contained in:
Jonas Hendrickx 2025-03-17 14:23:44 +01:00
parent 8768e69f76
commit 24826ec3bd
9 changed files with 77 additions and 108 deletions

@ -28,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IOrganizationAutomaticTaxStrategy _organizationAutomaticTaxStrategy;
public RemoveOrganizationFromProviderCommand( public RemoveOrganizationFromProviderCommand(
IEventService eventService, IEventService eventService,
@ -40,7 +41,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient) IPricingClient pricingClient,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
{ {
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
@ -53,6 +55,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_subscriberService = subscriberService; _subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy;
} }
public async Task RemoveOrganizationFromProvider( public async Task RemoveOrganizationFromProvider(
@ -107,7 +110,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
organization.IsValidClient() && organization.IsValidClient() &&
!string.IsNullOrEmpty(organization.GatewayCustomerId)) !string.IsNullOrEmpty(organization.GatewayCustomerId))
{ {
await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{ {
Description = string.Empty, Description = string.Empty,
Email = organization.BillingEmail Email = organization.BillingEmail
@ -120,7 +123,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Customer = organization.GatewayCustomerId, Customer = organization.GatewayCustomerId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30, DaysUntilDue = 30,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary<string, string> Metadata = new Dictionary<string, string>
{ {
{ "organizationId", organization.Id.ToString() } { "organizationId", organization.Id.ToString() }
@ -130,6 +132,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }]
}; };
await _organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);
var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); var subscription = await _stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
organization.GatewaySubscriptionId = subscription.Id; organization.GatewaySubscriptionId = subscription.Id;

@ -40,7 +40,8 @@ public class ProviderBillingService(
IProviderUserRepository providerUserRepository, IProviderUserRepository providerUserRepository,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService, ISubscriberService subscriberService,
ITaxService taxService) : IProviderBillingService ITaxService taxService,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy) : IProviderBillingService
{ {
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)] [RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task AddExistingOrganization( public async Task AddExistingOrganization(
@ -589,10 +590,6 @@ public class ProviderBillingService(
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
},
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Customer = customer.Id, Customer = customer.Id,
DaysUntilDue = 30, DaysUntilDue = 30,
@ -605,6 +602,8 @@ public class ProviderBillingService(
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
}; };
await organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);
try try
{ {
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);

@ -1,26 +0,0 @@
using Stripe;
namespace Bit.Core.Billing.Extensions;
public static class SubscriptionCreateOptionsExtensions
{
/// <summary>
/// Attempts to enable automatic tax for given new subscription options.
/// </summary>
/// <param name="options"></param>
/// <param name="customer">The existing customer.</param>
/// <returns>Returns true when successful, false when conditions are not met.</returns>
public static bool EnableAutomaticTax(this SubscriptionCreateOptions options, Customer customer)
{
// We might only need to check the automatic tax status.
if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country))
{
return false;
}
options.DefaultTaxRates = [];
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
return true;
}
}

@ -1,35 +0,0 @@
using Stripe;
namespace Bit.Core.Billing.Extensions;
public static class SubscriptionUpdateOptionsExtensions
{
/// <summary>
/// Attempts to enable automatic tax for given subscription options.
/// </summary>
/// <param name="options"></param>
/// <param name="customer">The existing customer to which the subscription belongs.</param>
/// <param name="subscription">The existing subscription.</param>
/// <returns>Returns true when successful, false when conditions are not met.</returns>
public static bool EnableAutomaticTax(
this SubscriptionUpdateOptions options,
Customer customer,
Subscription subscription)
{
if (subscription.AutomaticTax.Enabled)
{
return false;
}
// We might only need to check the automatic tax status.
if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country))
{
return false;
}
options.DefaultTaxRates = [];
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
return true;
}
}

@ -5,6 +5,6 @@ namespace Bit.Core.Billing.Services;
public interface IAutomaticTaxStrategy public interface IAutomaticTaxStrategy
{ {
Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription); Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription);
Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null); Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer);
Task SetUpdateOptionsAsync(SubscriptionUpdateOptions options, Subscription subscription); Task SetUpdateOptionsAsync(SubscriptionUpdateOptions options, Subscription subscription);
} }

@ -1,13 +1,20 @@
using Stripe; using Bit.Core.Billing.Constants;
using Stripe;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy
{ {
public Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null) public Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer)
{ {
ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(options);
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; ArgumentNullException.ThrowIfNull(customer);
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldEnable(customer)
};
return Task.CompletedTask; return Task.CompletedTask;
} }
@ -16,18 +23,23 @@ public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy
ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(options);
if (subscription.AutomaticTax.Enabled) if (subscription.AutomaticTax.Enabled == ShouldEnable(subscription.Customer))
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = ShouldEnable(subscription.Customer)
};
options.DefaultTaxRates = [];
return Task.CompletedTask; return Task.CompletedTask;
} }
public Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription) public Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription)
{ {
if (subscription.AutomaticTax.Enabled) if (subscription.AutomaticTax.Enabled == ShouldEnable(subscription.Customer))
{ {
return null; return null;
} }
@ -36,10 +48,16 @@ public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions AutomaticTax = new SubscriptionAutomaticTaxOptions
{ {
Enabled = true Enabled = ShouldEnable(subscription.Customer),
} },
DefaultTaxRates = []
}; };
return Task.FromResult(options); return Task.FromResult(options);
} }
private static bool ShouldEnable(Customer customer)
{
return customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
}
} }

@ -8,6 +8,15 @@ namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class OrganizationAutomaticTaxStrategy( public class OrganizationAutomaticTaxStrategy(
IPricingClient pricingClient) : IOrganizationAutomaticTaxStrategy IPricingClient pricingClient) : IOrganizationAutomaticTaxStrategy
{ {
private readonly Lazy<Task<IEnumerable<string>>> _familyPriceIdsTask = new(async () =>
{
var plans = await Task.WhenAll(
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually));
return plans.Select(plan => plan.PasswordManager.StripePlanId);
});
public async Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription) public async Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription)
{ {
ArgumentNullException.ThrowIfNull(subscription); ArgumentNullException.ThrowIfNull(subscription);
@ -23,13 +32,14 @@ public class OrganizationAutomaticTaxStrategy(
AutomaticTax = new SubscriptionAutomaticTaxOptions AutomaticTax = new SubscriptionAutomaticTaxOptions
{ {
Enabled = isEnabled.Value Enabled = isEnabled.Value
} },
DefaultTaxRates = []
}; };
return options; return options;
} }
public async Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null) public async Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer)
{ {
ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(customer); ArgumentNullException.ThrowIfNull(customer);
@ -59,6 +69,7 @@ public class OrganizationAutomaticTaxStrategy(
{ {
Enabled = isEnabled.Value Enabled = isEnabled.Value
}; };
options.DefaultTaxRates = [];
} }
private async Task<bool?> IsEnabledAsync(Subscription subscription) private async Task<bool?> IsEnabledAsync(Subscription subscription)
@ -75,13 +86,9 @@ public class OrganizationAutomaticTaxStrategy(
private async Task<bool> IsNonTaxableNonUsBusinessUseSubscriptionAsync(Subscription subscription) private async Task<bool> IsNonTaxableNonUsBusinessUseSubscriptionAsync(Subscription subscription)
{ {
var familyPriceIds = (await Task.WhenAll( var familyPriceIds = await _familyPriceIdsTask.Value;
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);
return subscription.Customer.Address.Country != "US" && return subscription.Customer.Address.Country != "US" &&
subscription.IsOrganization() &&
!subscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() && !subscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
!subscription.Customer.TaxIds.Any(); !subscription.Customer.TaxIds.Any();
} }
@ -99,10 +106,7 @@ public class OrganizationAutomaticTaxStrategy(
private async Task<bool> IsNonTaxableNonUsBusinessUseSubscriptionAsync(SubscriptionCreateOptions options, Customer customer) private async Task<bool> IsNonTaxableNonUsBusinessUseSubscriptionAsync(SubscriptionCreateOptions options, Customer customer)
{ {
var familyPriceIds = (await Task.WhenAll( var familyPriceIds = await _familyPriceIdsTask.Value;
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019),
pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually)))
.Select(plan => plan.PasswordManager.StripePlanId);
return customer.Address.Country != "US" && return customer.Address.Country != "US" &&
!options.Items.Select(item => item.Price).Intersect(familyPriceIds).Any() && !options.Items.Select(item => item.Price).Intersect(familyPriceIds).Any() &&

@ -25,7 +25,8 @@ public class PremiumUserBillingService(
ISetupIntentCache setupIntentCache, ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IUserRepository userRepository) : IPremiumUserBillingService IUserRepository userRepository,
IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy) : IPremiumUserBillingService
{ {
public async Task Credit(User user, decimal amount) public async Task Credit(User user, decimal amount)
{ {
@ -318,10 +319,6 @@ public class PremiumUserBillingService(
var subscriptionCreateOptions = new SubscriptionCreateOptions var subscriptionCreateOptions = new SubscriptionCreateOptions
{ {
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported,
},
CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically,
Customer = customer.Id, Customer = customer.Id,
Items = subscriptionItemOptionsList, Items = subscriptionItemOptionsList,
@ -335,6 +332,8 @@ public class PremiumUserBillingService(
OffSession = true OffSession = true
}; };
await individualAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
if (usingPayPal) if (usingPayPal)

@ -36,6 +36,8 @@ public class StripePaymentService : IPaymentService
private readonly ITaxService _taxService; private readonly ITaxService _taxService;
private readonly ISubscriberService _subscriberService; private readonly ISubscriberService _subscriberService;
private readonly IPricingClient _pricingClient; private readonly IPricingClient _pricingClient;
private readonly IIndividualAutomaticTaxStrategy _individualAutomaticTaxStrategy;
private readonly IOrganizationAutomaticTaxStrategy _organizationAutomaticTaxStrategy;
public StripePaymentService( public StripePaymentService(
ITransactionRepository transactionRepository, ITransactionRepository transactionRepository,
@ -46,7 +48,9 @@ public class StripePaymentService : IPaymentService
IFeatureService featureService, IFeatureService featureService,
ITaxService taxService, ITaxService taxService,
ISubscriberService subscriberService, ISubscriberService subscriberService,
IPricingClient pricingClient) IPricingClient pricingClient,
IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
{ {
_transactionRepository = transactionRepository; _transactionRepository = transactionRepository;
_logger = logger; _logger = logger;
@ -57,6 +61,8 @@ public class StripePaymentService : IPaymentService
_taxService = taxService; _taxService = taxService;
_subscriberService = subscriberService; _subscriberService = subscriberService;
_pricingClient = pricingClient; _pricingClient = pricingClient;
_individualAutomaticTaxStrategy = individualAutomaticTaxStrategy;
_organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy;
} }
private async Task ChangeOrganizationSponsorship( private async Task ChangeOrganizationSponsorship(
@ -124,7 +130,7 @@ public class StripePaymentService : IPaymentService
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" }; new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
} }
subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); await _organizationAutomaticTaxStrategy.SetUpdateOptionsAsync(subUpdateOptions, sub);
if (!subscriptionUpdate.UpdateNeeded(sub)) if (!subscriptionUpdate.UpdateNeeded(sub))
{ {
@ -812,20 +818,20 @@ public class StripePaymentService : IPaymentService
} }
if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
customer.Subscriptions.Any(sub => customer.Subscriptions.Any(sub => sub.Id == subscriber.GatewaySubscriptionId))
sub.Id == subscriber.GatewaySubscriptionId &&
!sub.AutomaticTax.Enabled) &&
customer.HasTaxLocationVerified())
{ {
var subscriptionUpdateOptions = new SubscriptionUpdateOptions var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId);
{
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
DefaultTaxRates = []
};
_ = await _stripeAdapter.SubscriptionUpdateAsync( var subscriptionUpdateOptions = subscriber is User
subscriber.GatewaySubscriptionId, ? await _individualAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription)
subscriptionUpdateOptions); : await _organizationAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription);
if (subscriptionUpdateOptions != null)
{
_ = await _stripeAdapter.SubscriptionUpdateAsync(
subscriber.GatewaySubscriptionId,
subscriptionUpdateOptions);
}
} }
} }
catch catch