1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-16 02:28:13 -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

View File

@ -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,
@ -40,7 +41,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IProviderBillingService providerBillingService,
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
IPricingClient pricingClient)
IPricingClient pricingClient,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
{
_eventService = eventService;
_mailService = mailService;
@ -53,6 +55,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
_pricingClient = pricingClient;
_organizationAutomaticTaxStrategy = organizationAutomaticTaxStrategy;
}
public async Task RemoveOrganizationFromProvider(
@ -107,7 +110,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
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
@ -120,7 +123,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
Customer = organization.GatewayCustomerId,
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
DaysUntilDue = 30,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
Metadata = new Dictionary<string, string>
{
{ "organizationId", organization.Id.ToString() }
@ -130,6 +132,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
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;

View File

@ -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(
@ -589,10 +590,6 @@ public class ProviderBillingService(
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
},
CollectionMethod = StripeConstants.CollectionMethod.SendInvoice,
Customer = customer.Id,
DaysUntilDue = 30,
@ -605,6 +602,8 @@ public class ProviderBillingService(
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations
};
await organizationAutomaticTaxStrategy.SetCreateOptionsAsync(subscriptionCreateOptions, customer);
try
{
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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