1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 21:18:13 -05:00
This commit is contained in:
Jonas Hendrickx 2025-03-17 13:47:03 +01:00
parent 1b90bfe2a1
commit 8768e69f76
8 changed files with 196 additions and 27 deletions

View File

@ -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;
@ -21,7 +20,9 @@ public class UpcomingInvoiceHandler(
IStripeEventService stripeEventService,
IStripeEventUtilityService stripeEventUtilityService,
IUserRepository userRepository,
IValidateSponsorshipCommand validateSponsorshipCommand)
IValidateSponsorshipCommand validateSponsorshipCommand,
IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy,
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
: IUpcomingInvoiceHandler
{
public async Task HandleAsync(Event parsedEvent)
@ -136,33 +137,15 @@ public class UpcomingInvoiceHandler(
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
{
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
var updateOptions = subscription.IsOrganization()
? await organizationAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription)
: await individualAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription);
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);
}
}

View File

@ -47,6 +47,8 @@ public static class StripeConstants
public static class MetadataKeys
{
public const string OrganizationId = "organizationId";
public const string ProviderId = "providerId";
public const string UserId = "userId";
}
public static class PaymentBehavior

View File

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

View File

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

View File

@ -0,0 +1,3 @@
namespace Bit.Core.Billing.Services;
public interface IIndividualAutomaticTaxStrategy : IAutomaticTaxStrategy;

View File

@ -0,0 +1,3 @@
namespace Bit.Core.Billing.Services;
public interface IOrganizationAutomaticTaxStrategy : IAutomaticTaxStrategy;

View File

@ -0,0 +1,45 @@
using Stripe;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class IndividualAutomaticTaxStrategy : IIndividualAutomaticTaxStrategy
{
public Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null)
{
ArgumentNullException.ThrowIfNull(options);
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
return Task.CompletedTask;
}
public Task SetUpdateOptionsAsync(SubscriptionUpdateOptions options, Subscription subscription)
{
ArgumentNullException.ThrowIfNull(options);
if (subscription.AutomaticTax.Enabled)
{
return Task.CompletedTask;
}
options.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
return Task.CompletedTask;
}
public Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription)
{
if (subscription.AutomaticTax.Enabled)
{
return null;
}
var options = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = true
}
};
return Task.FromResult(options);
}
}

View File

@ -0,0 +1,111 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Pricing;
using Stripe;
namespace Bit.Core.Billing.Services.Implementations.AutomaticTax;
public class OrganizationAutomaticTaxStrategy(
IPricingClient pricingClient) : IOrganizationAutomaticTaxStrategy
{
public async Task<SubscriptionUpdateOptions> GetUpdateOptionsAsync(Subscription subscription)
{
ArgumentNullException.ThrowIfNull(subscription);
var isEnabled = await IsEnabledAsync(subscription);
if (!isEnabled.HasValue)
{
return null;
}
var options = new SubscriptionUpdateOptions
{
AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = isEnabled.Value
}
};
return options;
}
public async Task SetCreateOptionsAsync(SubscriptionCreateOptions options, Customer customer = null)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(customer);
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = await IsEnabledAsync(options, customer)
};
}
public async Task SetUpdateOptionsAsync(SubscriptionUpdateOptions options, Subscription subscription)
{
ArgumentNullException.ThrowIfNull(subscription);
if (subscription.AutomaticTax.Enabled == options.AutomaticTax?.Enabled)
{
return;
}
var isEnabled = await IsEnabledAsync(subscription);
if (!isEnabled.HasValue)
{
return;
}
options.AutomaticTax = new SubscriptionAutomaticTaxOptions
{
Enabled = isEnabled.Value
};
}
private async Task<bool?> IsEnabledAsync(Subscription subscription)
{
if (subscription.AutomaticTax.Enabled ||
!subscription.Customer.HasBillingLocation() ||
await IsNonTaxableNonUsBusinessUseSubscriptionAsync(subscription))
{
return null;
}
return !await IsNonTaxableNonUsBusinessUseSubscriptionAsync(subscription);
}
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);
return subscription.Customer.Address.Country != "US" &&
subscription.IsOrganization() &&
!subscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() &&
!subscription.Customer.TaxIds.Any();
}
private async Task<bool?> IsEnabledAsync(SubscriptionCreateOptions options, Customer customer)
{
if (!customer.HasBillingLocation() ||
await IsNonTaxableNonUsBusinessUseSubscriptionAsync(options, customer))
{
return null;
}
return !await IsNonTaxableNonUsBusinessUseSubscriptionAsync(options, customer);
}
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);
return customer.Address.Country != "US" &&
!options.Items.Select(item => item.Price).Intersect(familyPriceIds).Any() &&
!customer.TaxIds.Any();
}
}