mirror of
https://github.com/bitwarden/server.git
synced 2025-04-06 21:48:12 -05:00
WIP
This commit is contained in:
parent
1b90bfe2a1
commit
8768e69f76
@ -1,8 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Constants;
|
|
||||||
using Bit.Core.Billing.Enums;
|
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
@ -21,7 +20,9 @@ public class UpcomingInvoiceHandler(
|
|||||||
IStripeEventService stripeEventService,
|
IStripeEventService stripeEventService,
|
||||||
IStripeEventUtilityService stripeEventUtilityService,
|
IStripeEventUtilityService stripeEventUtilityService,
|
||||||
IUserRepository userRepository,
|
IUserRepository userRepository,
|
||||||
IValidateSponsorshipCommand validateSponsorshipCommand)
|
IValidateSponsorshipCommand validateSponsorshipCommand,
|
||||||
|
IIndividualAutomaticTaxStrategy individualAutomaticTaxStrategy,
|
||||||
|
IOrganizationAutomaticTaxStrategy organizationAutomaticTaxStrategy)
|
||||||
: IUpcomingInvoiceHandler
|
: IUpcomingInvoiceHandler
|
||||||
{
|
{
|
||||||
public async Task HandleAsync(Event parsedEvent)
|
public async Task HandleAsync(Event parsedEvent)
|
||||||
@ -136,33 +137,15 @@ public class UpcomingInvoiceHandler(
|
|||||||
|
|
||||||
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
|
private async Task TryEnableAutomaticTaxAsync(Subscription subscription)
|
||||||
{
|
{
|
||||||
if (subscription.AutomaticTax.Enabled ||
|
var updateOptions = subscription.IsOrganization()
|
||||||
!subscription.Customer.HasBillingLocation() ||
|
? await organizationAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription)
|
||||||
await IsNonTaxableNonUSBusinessUseSubscription(subscription))
|
: await individualAutomaticTaxStrategy.GetUpdateOptionsAsync(subscription);
|
||||||
|
|
||||||
|
if (updateOptions == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await stripeFacade.UpdateSubscription(subscription.Id,
|
await stripeFacade.UpdateSubscription(subscription.Id, updateOptions);
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,8 @@ public static class StripeConstants
|
|||||||
public static class MetadataKeys
|
public static class MetadataKeys
|
||||||
{
|
{
|
||||||
public const string OrganizationId = "organizationId";
|
public const string OrganizationId = "organizationId";
|
||||||
|
public const string ProviderId = "providerId";
|
||||||
|
public const string UserId = "userId";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class PaymentBehavior
|
public static class PaymentBehavior
|
||||||
|
12
src/Core/Billing/Extensions/SubscriptionExtensions.cs
Normal file
12
src/Core/Billing/Extensions/SubscriptionExtensions.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
10
src/Core/Billing/Services/IAutomaticTaxStrategy.cs
Normal file
10
src/Core/Billing/Services/IAutomaticTaxStrategy.cs
Normal 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);
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace Bit.Core.Billing.Services;
|
||||||
|
|
||||||
|
public interface IIndividualAutomaticTaxStrategy : IAutomaticTaxStrategy;
|
@ -0,0 +1,3 @@
|
|||||||
|
namespace Bit.Core.Billing.Services;
|
||||||
|
|
||||||
|
public interface IOrganizationAutomaticTaxStrategy : IAutomaticTaxStrategy;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user