mirror of
https://github.com/bitwarden/server.git
synced 2025-07-04 01:22:50 -05:00
[PM-5766] Automatic Tax Feature Flag (#3729)
* Added feature flag constant * Wrapped Automatic Tax logic behind feature flag * Only getting customer if feature is anabled. * Enabled feature flag in unit tests * Made IPaymentService scoped * Added missing StripeFacade calls
This commit is contained in:
@ -1,6 +1,7 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Billing.Models;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -23,6 +24,7 @@ using Event = Stripe.Event;
|
||||
using JsonSerializer = System.Text.Json.JsonSerializer;
|
||||
using PaymentMethod = Stripe.PaymentMethod;
|
||||
using Subscription = Stripe.Subscription;
|
||||
using TaxRate = Bit.Core.Entities.TaxRate;
|
||||
using Transaction = Bit.Core.Entities.Transaction;
|
||||
using TransactionType = Bit.Core.Enums.TransactionType;
|
||||
|
||||
@ -52,6 +54,7 @@ public class StripeController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public StripeController(
|
||||
GlobalSettings globalSettings,
|
||||
@ -70,7 +73,8 @@ public class StripeController : Controller
|
||||
IUserRepository userRepository,
|
||||
ICurrentContext currentContext,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeFacade stripeFacade)
|
||||
IStripeFacade stripeFacade,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_billingSettings = billingSettings?.Value;
|
||||
_hostingEnvironment = hostingEnvironment;
|
||||
@ -97,6 +101,7 @@ public class StripeController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeFacade = stripeFacade;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpPost("webhook")]
|
||||
@ -242,17 +247,29 @@ public class StripeController : Controller
|
||||
$"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'");
|
||||
}
|
||||
|
||||
if (!subscription.AutomaticTax.Enabled)
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
{
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
DefaultTaxRates = new List<string>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
});
|
||||
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId);
|
||||
if (!subscription.AutomaticTax.Enabled &&
|
||||
!string.IsNullOrEmpty(customer.Address?.PostalCode) &&
|
||||
!string.IsNullOrEmpty(customer.Address?.Country))
|
||||
{
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
DefaultTaxRates = new List<string>(),
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var (organizationId, userId) = GetIdsFromMetaData(subscription.Metadata);
|
||||
|
||||
var updatedSubscription = pm5766AutomaticTaxIsEnabled
|
||||
? subscription
|
||||
: await VerifyCorrectTaxRateForCharge(invoice, subscription);
|
||||
|
||||
var (organizationId, userId) = GetIdsFromMetaData(updatedSubscription.Metadata);
|
||||
|
||||
var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList();
|
||||
|
||||
@ -273,7 +290,7 @@ public class StripeController : Controller
|
||||
|
||||
if (organizationId.HasValue)
|
||||
{
|
||||
if (IsSponsoredSubscription(subscription))
|
||||
if (IsSponsoredSubscription(updatedSubscription))
|
||||
{
|
||||
await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
|
||||
}
|
||||
@ -321,22 +338,20 @@ public class StripeController : Controller
|
||||
|
||||
Tuple<Guid?, Guid?> ids = null;
|
||||
Subscription subscription = null;
|
||||
var subscriptionService = new SubscriptionService();
|
||||
|
||||
if (charge.InvoiceId != null)
|
||||
{
|
||||
var invoiceService = new InvoiceService();
|
||||
var invoice = await invoiceService.GetAsync(charge.InvoiceId);
|
||||
var invoice = await _stripeFacade.GetInvoice(charge.InvoiceId);
|
||||
if (invoice?.SubscriptionId != null)
|
||||
{
|
||||
subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
||||
ids = GetIdsFromMetaData(subscription?.Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription == null || ids == null || (ids.Item1.HasValue && ids.Item2.HasValue))
|
||||
{
|
||||
var subscriptions = await subscriptionService.ListAsync(new SubscriptionListOptions
|
||||
var subscriptions = await _stripeFacade.ListSubscriptions(new SubscriptionListOptions
|
||||
{
|
||||
Customer = charge.CustomerId
|
||||
});
|
||||
@ -490,8 +505,7 @@ public class StripeController : Controller
|
||||
var invoice = await _stripeEventService.GetInvoice(parsedEvent, true);
|
||||
if (invoice.Paid && invoice.BillingReason == "subscription_create")
|
||||
{
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
||||
if (subscription?.Status == StripeSubscriptionStatus.Active)
|
||||
{
|
||||
if (DateTime.UtcNow - invoice.Created < TimeSpan.FromMinutes(1))
|
||||
@ -596,7 +610,6 @@ public class StripeController : Controller
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscriptionListOptions = new SubscriptionListOptions
|
||||
{
|
||||
Customer = paymentMethod.CustomerId,
|
||||
@ -607,7 +620,7 @@ public class StripeController : Controller
|
||||
StripeList<Subscription> unpaidSubscriptions;
|
||||
try
|
||||
{
|
||||
unpaidSubscriptions = await subscriptionService.ListAsync(subscriptionListOptions);
|
||||
unpaidSubscriptions = await _stripeFacade.ListSubscriptions(subscriptionListOptions);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -702,8 +715,7 @@ public class StripeController : Controller
|
||||
|
||||
private async Task<bool> AttemptToPayInvoiceAsync(Invoice invoice, bool attemptToPayWithStripe = false)
|
||||
{
|
||||
var customerService = new CustomerService();
|
||||
var customer = await customerService.GetAsync(invoice.CustomerId);
|
||||
var customer = await _stripeFacade.GetCustomer(invoice.CustomerId);
|
||||
|
||||
if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false)
|
||||
{
|
||||
@ -728,8 +740,7 @@ public class StripeController : Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
||||
var ids = GetIdsFromMetaData(subscription?.Metadata);
|
||||
if (!ids.Item1.HasValue && !ids.Item2.HasValue)
|
||||
{
|
||||
@ -797,10 +808,9 @@ public class StripeController : Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
var invoiceService = new InvoiceService();
|
||||
try
|
||||
{
|
||||
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
await _stripeFacade.UpdateInvoice(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
@ -809,14 +819,14 @@ public class StripeController : Controller
|
||||
transactionResult.Target.PayPalDetails?.AuthorizationId
|
||||
}
|
||||
});
|
||||
await invoiceService.PayAsync(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });
|
||||
await _stripeFacade.PayInvoice(invoice.Id, new InvoicePayOptions { PaidOutOfBand = true });
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
await _btGateway.Transaction.RefundAsync(transactionResult.Target.Id);
|
||||
if (e.Message.Contains("Invoice is already paid"))
|
||||
{
|
||||
await invoiceService.UpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
await _stripeFacade.UpdateInvoice(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = invoice.Metadata
|
||||
});
|
||||
@ -834,8 +844,7 @@ public class StripeController : Controller
|
||||
{
|
||||
try
|
||||
{
|
||||
var invoiceService = new InvoiceService();
|
||||
await invoiceService.PayAsync(invoice.Id);
|
||||
await _stripeFacade.PayInvoice(invoice.Id);
|
||||
return true;
|
||||
}
|
||||
catch (Exception e)
|
||||
@ -855,6 +864,41 @@ public class StripeController : Controller
|
||||
invoice.BillingReason == "subscription_cycle" && invoice.SubscriptionId != null;
|
||||
}
|
||||
|
||||
private async Task<Subscription> VerifyCorrectTaxRateForCharge(Invoice invoice, Subscription subscription)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.Country) ||
|
||||
string.IsNullOrWhiteSpace(invoice?.CustomerAddress?.PostalCode))
|
||||
{
|
||||
return subscription;
|
||||
}
|
||||
|
||||
var localBitwardenTaxRates = await _taxRateRepository.GetByLocationAsync(
|
||||
new TaxRate()
|
||||
{
|
||||
Country = invoice.CustomerAddress.Country,
|
||||
PostalCode = invoice.CustomerAddress.PostalCode
|
||||
}
|
||||
);
|
||||
|
||||
if (!localBitwardenTaxRates.Any())
|
||||
{
|
||||
return subscription;
|
||||
}
|
||||
|
||||
var stripeTaxRate = await _stripeFacade.GetTaxRate(localBitwardenTaxRates.First().Id);
|
||||
if (stripeTaxRate == null || subscription.DefaultTaxRates.Any(x => x == stripeTaxRate))
|
||||
{
|
||||
return subscription;
|
||||
}
|
||||
|
||||
subscription.DefaultTaxRates = new List<Stripe.TaxRate> { stripeTaxRate };
|
||||
|
||||
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = new List<string> { stripeTaxRate.Id } };
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions);
|
||||
|
||||
return subscription;
|
||||
}
|
||||
|
||||
private static bool IsSponsoredSubscription(Subscription subscription) =>
|
||||
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);
|
||||
|
||||
@ -862,8 +906,7 @@ public class StripeController : Controller
|
||||
{
|
||||
if (!invoice.Paid && invoice.AttemptCount > 1 && UnpaidAutoChargeInvoiceForSubscriptionCycle(invoice))
|
||||
{
|
||||
var subscriptionService = new SubscriptionService();
|
||||
var subscription = await subscriptionService.GetAsync(invoice.SubscriptionId);
|
||||
var subscription = await _stripeFacade.GetSubscription(invoice.SubscriptionId);
|
||||
// attempt count 4 = 11 days after initial failure
|
||||
if (invoice.AttemptCount <= 3 ||
|
||||
!subscription.Items.Any(i => i.Price.Id is PremiumPlanId or PremiumPlanIdAppStore))
|
||||
@ -873,23 +916,20 @@ public class StripeController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task CancelSubscription(string subscriptionId)
|
||||
{
|
||||
await new SubscriptionService().CancelAsync(subscriptionId, new SubscriptionCancelOptions());
|
||||
}
|
||||
private async Task CancelSubscription(string subscriptionId) =>
|
||||
await _stripeFacade.CancelSubscription(subscriptionId, new SubscriptionCancelOptions());
|
||||
|
||||
private async Task VoidOpenInvoices(string subscriptionId)
|
||||
{
|
||||
var invoiceService = new InvoiceService();
|
||||
var options = new InvoiceListOptions
|
||||
{
|
||||
Status = StripeInvoiceStatus.Open,
|
||||
Subscription = subscriptionId
|
||||
};
|
||||
var invoices = invoiceService.List(options);
|
||||
var invoices = await _stripeFacade.ListInvoices(options);
|
||||
foreach (var invoice in invoices)
|
||||
{
|
||||
await invoiceService.VoidInvoiceAsync(invoice.Id);
|
||||
await _stripeFacade.VoidInvoice(invoice.Id);
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user