1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -05:00

[PM-5766] Enabled Automatic Tax for all customers (#3685)

* Removed TaxRate logic when creating or updating a Stripe subscription and replaced it with AutomaticTax enabled flag

* Updated Stripe webhook to update subscription to automatically calculate tax

* Removed TaxRate unit tests since Stripe now handles tax

* Removed test proration logic

* Including taxInfo when updating payment method

* Adding the address to the upgrade free org flow if it doesn't exist

* Fixed failing tests and added a new test to validate that the customer is updated
This commit is contained in:
Conner Turnbull
2024-01-29 09:48:59 -05:00
committed by GitHub
parent c2b4ee7eac
commit a2e6550b61
6 changed files with 127 additions and 167 deletions

View File

@ -21,7 +21,6 @@ using Customer = Stripe.Customer;
using Event = Stripe.Event;
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;
@ -223,9 +222,17 @@ public class StripeController : Controller
$"Received null Subscription from Stripe for ID '{invoice.SubscriptionId}' while processing Event with ID '{parsedEvent.Id}'");
}
var updatedSubscription = await VerifyCorrectTaxRateForCharge(invoice, subscription);
if (!subscription.AutomaticTax.Enabled)
{
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
new SubscriptionUpdateOptions
{
DefaultTaxRates = new List<string>(),
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
});
}
var (organizationId, userId) = GetIdsFromMetaData(updatedSubscription.Metadata);
var (organizationId, userId) = GetIdsFromMetaData(subscription.Metadata);
var invoiceLineItemDescriptions = invoice.Lines.Select(i => i.Description).ToList();
@ -246,7 +253,7 @@ public class StripeController : Controller
if (organizationId.HasValue)
{
if (IsSponsoredSubscription(updatedSubscription))
if (IsSponsoredSubscription(subscription))
{
await _validateSponsorshipCommand.ValidateSponsorshipAsync(organizationId.Value);
}
@ -828,32 +835,6 @@ 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))
{
var localBitwardenTaxRates = await _taxRateRepository.GetByLocationAsync(
new TaxRate()
{
Country = invoice.CustomerAddress.Country,
PostalCode = invoice.CustomerAddress.PostalCode
}
);
if (localBitwardenTaxRates.Any())
{
var stripeTaxRate = await new TaxRateService().GetAsync(localBitwardenTaxRates.First().Id);
if (stripeTaxRate != null && !subscription.DefaultTaxRates.Any(x => x == stripeTaxRate))
{
subscription.DefaultTaxRates = new List<Stripe.TaxRate> { stripeTaxRate };
var subscriptionOptions = new SubscriptionUpdateOptions() { DefaultTaxRates = new List<string>() { stripeTaxRate.Id } };
subscription = await new SubscriptionService().UpdateAsync(subscription.Id, subscriptionOptions);
}
}
}
return subscription;
}
private static bool IsSponsoredSubscription(Subscription subscription) =>
StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id);

View File

@ -33,4 +33,10 @@ public interface IStripeFacade
SubscriptionGetOptions subscriptionGetOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);
Task<Subscription> UpdateSubscription(
string subscriptionId,
SubscriptionUpdateOptions subscriptionGetOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default);
}

View File

@ -44,4 +44,11 @@ public class StripeFacade : IStripeFacade
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default) =>
await _subscriptionService.GetAsync(subscriptionId, subscriptionGetOptions, requestOptions, cancellationToken);
public async Task<Subscription> UpdateSubscription(
string subscriptionId,
SubscriptionUpdateOptions subscriptionUpdateOptions = null,
RequestOptions requestOptions = null,
CancellationToken cancellationToken = default) =>
await _subscriptionService.UpdateAsync(subscriptionId, subscriptionUpdateOptions, requestOptions, cancellationToken);
}

View File

@ -140,7 +140,7 @@ public class OrganizationService : IOrganizationService
await _paymentService.SaveTaxInfoAsync(organization, taxInfo);
var updated = await _paymentService.UpdatePaymentMethodAsync(organization,
paymentMethodType, paymentToken);
paymentMethodType, paymentToken, taxInfo);
if (updated)
{
await ReplaceAndUpdateCacheAsync(organization);

View File

@ -98,23 +98,6 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Payment method is not supported at this time.");
}
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
{
var taxRateSearch = new TaxRate
{
Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode
};
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);
// should only be one tax rate per country/zip combo
var taxRate = taxRates.FirstOrDefault();
if (taxRate != null)
{
taxInfo.StripeTaxRateId = taxRate.Id;
}
}
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
, additionalSmSeats, additionalServiceAccount);
@ -163,6 +146,9 @@ public class StripePaymentService : IPaymentService
});
subCreateOptions.AddExpand("latest_invoice.payment_intent");
subCreateOptions.Customer = customer.Id;
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true };
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
{
@ -244,25 +230,31 @@ public class StripePaymentService : IPaymentService
throw new GatewayException("Could not find customer payment profile.");
}
var taxInfo = upgrade.TaxInfo;
if (taxInfo != null && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressCountry) && !string.IsNullOrWhiteSpace(taxInfo.BillingAddressPostalCode))
if (customer.Address is null &&
!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressCountry) &&
!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressPostalCode))
{
var taxRateSearch = new TaxRate
var addressOptions = new Stripe.AddressOptions
{
Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode
Country = upgrade.TaxInfo.BillingAddressCountry,
PostalCode = upgrade.TaxInfo.BillingAddressPostalCode,
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
Line1 = upgrade.TaxInfo.BillingAddressLine1 ?? string.Empty,
Line2 = upgrade.TaxInfo.BillingAddressLine2,
City = upgrade.TaxInfo.BillingAddressCity,
State = upgrade.TaxInfo.BillingAddressState,
};
var taxRates = await _taxRateRepository.GetByLocationAsync(taxRateSearch);
// should only be one tax rate per country/zip combo
var taxRate = taxRates.FirstOrDefault();
if (taxRate != null)
{
taxInfo.StripeTaxRateId = taxRate.Id;
}
var customerUpdateOptions = new Stripe.CustomerUpdateOptions { Address = addressOptions };
customerUpdateOptions.AddExpand("default_source");
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
}
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade)
{
DefaultTaxRates = new List<string>(),
AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true }
};
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
@ -459,26 +451,6 @@ public class StripePaymentService : IPaymentService
Quantity = 1
});
if (!string.IsNullOrWhiteSpace(taxInfo?.BillingAddressCountry)
&& !string.IsNullOrWhiteSpace(taxInfo?.BillingAddressPostalCode))
{
var taxRates = await _taxRateRepository.GetByLocationAsync(
new TaxRate()
{
Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode
}
);
var taxRate = taxRates.FirstOrDefault();
if (taxRate != null)
{
subCreateOptions.DefaultTaxRates = new List<string>(1)
{
taxRate.Id
};
}
}
if (additionalStorageGb > 0)
{
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
@ -488,6 +460,8 @@ public class StripePaymentService : IPaymentService
});
}
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true };
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
@ -525,7 +499,8 @@ public class StripePaymentService : IPaymentService
{
Customer = customer.Id,
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
AutomaticTax =
new Stripe.InvoiceAutomaticTaxOptions { Enabled = subCreateOptions.AutomaticTax.Enabled }
});
if (previewInvoice.AmountDue > 0)
@ -583,7 +558,8 @@ public class StripePaymentService : IPaymentService
{
Customer = customer.Id,
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
AutomaticTax =
new Stripe.InvoiceAutomaticTaxOptions { Enabled = subCreateOptions.AutomaticTax.Enabled }
});
if (previewInvoice.AmountDue > 0)
{
@ -593,6 +569,7 @@ public class StripePaymentService : IPaymentService
subCreateOptions.OffSession = true;
subCreateOptions.AddExpand("latest_invoice.payment_intent");
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true };
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
{
@ -692,6 +669,8 @@ public class StripePaymentService : IPaymentService
DaysUntilDue = daysUntilDue ?? 1,
CollectionMethod = "send_invoice",
ProrationDate = prorationDate,
DefaultTaxRates = new List<string>(),
AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true }
};
if (!subscriptionUpdate.UpdateNeeded(sub))
@ -700,28 +679,6 @@ public class StripePaymentService : IPaymentService
return null;
}
var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId);
if (!string.IsNullOrWhiteSpace(customer?.Address?.Country)
&& !string.IsNullOrWhiteSpace(customer?.Address?.PostalCode))
{
var taxRates = await _taxRateRepository.GetByLocationAsync(
new TaxRate()
{
Country = customer.Address.Country,
PostalCode = customer.Address.PostalCode
}
);
var taxRate = taxRates.FirstOrDefault();
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
{
subUpdateOptions.DefaultTaxRates = new List<string>(1)
{
taxRate.Id
};
}
}
string paymentIntentClientSecret = null;
try
{