1
0
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:
Conner Turnbull
2024-02-01 13:21:17 -05:00
committed by GitHub
parent 34c4a5df5d
commit 9a1519f131
8 changed files with 318 additions and 64 deletions

View File

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