mirror of
https://github.com/bitwarden/server.git
synced 2024-12-22 16:57:36 +01:00
[AC-2239] fix automatic tax errors (#3834)
* Ensuring customer has address before enabling automatic tax * StripeController fixes * Refactored automatic tax logic to use customer's automatic tax values * Downgraded refund error in paypal controller to be a warning * Resolved broken test after downgrading error to warning * Resolved broken paypal unit tests on windows machines --------- Co-authored-by: Lotus Scott <148992878+lscottbw@users.noreply.github.com>
This commit is contained in:
parent
9d59e4dc9e
commit
2dc068a983
@ -204,8 +204,8 @@ public class PayPalController : Controller
|
||||
|
||||
if (parentTransaction == null)
|
||||
{
|
||||
_logger.LogError("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
||||
return BadRequest();
|
||||
_logger.LogWarning("PayPal IPN ({Id}): Could not find parent transaction", transactionModel.TransactionId);
|
||||
return Ok();
|
||||
}
|
||||
|
||||
var refundAmount = Math.Abs(transactionModel.MerchantGross);
|
||||
|
@ -3,6 +3,7 @@ using Bit.Billing.Models;
|
||||
using Bit.Billing.Services;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
@ -188,7 +189,7 @@ public class StripeController : Controller
|
||||
}
|
||||
|
||||
var user = await _userService.GetUserByIdAsync(userId);
|
||||
if (user.Premium)
|
||||
if (user?.Premium == true)
|
||||
{
|
||||
await _userService.DisablePremiumAsync(userId, subscription.CurrentPeriodEnd);
|
||||
}
|
||||
@ -250,21 +251,21 @@ public class StripeController : Controller
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
{
|
||||
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId);
|
||||
var customerGetOptions = new CustomerGetOptions();
|
||||
customerGetOptions.AddExpand("tax");
|
||||
var customer = await _stripeFacade.GetCustomer(subscription.CustomerId, customerGetOptions);
|
||||
if (!subscription.AutomaticTax.Enabled &&
|
||||
!string.IsNullOrEmpty(customer.Address?.PostalCode) &&
|
||||
!string.IsNullOrEmpty(customer.Address?.Country))
|
||||
customer.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported)
|
||||
{
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id,
|
||||
new SubscriptionUpdateOptions
|
||||
{
|
||||
DefaultTaxRates = new List<string>(),
|
||||
DefaultTaxRates = [],
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var updatedSubscription = pm5766AutomaticTaxIsEnabled
|
||||
? subscription
|
||||
: await VerifyCorrectTaxRateForCharge(invoice, subscription);
|
||||
@ -319,7 +320,7 @@ public class StripeController : Controller
|
||||
{
|
||||
var user = await _userService.GetUserByIdAsync(userId.Value);
|
||||
|
||||
if (user.Premium)
|
||||
if (user?.Premium == true)
|
||||
{
|
||||
await SendEmails(new List<string> { user.Email });
|
||||
}
|
||||
@ -571,7 +572,7 @@ public class StripeController : Controller
|
||||
else if (parsedEvent.Type.Equals(HandledStripeWebhook.CustomerUpdated))
|
||||
{
|
||||
var customer =
|
||||
await _stripeEventService.GetCustomer(parsedEvent, true, new List<string> { "subscriptions" });
|
||||
await _stripeEventService.GetCustomer(parsedEvent, true, ["subscriptions"]);
|
||||
|
||||
if (customer.Subscriptions == null || !customer.Subscriptions.Any())
|
||||
{
|
||||
@ -614,7 +615,7 @@ public class StripeController : Controller
|
||||
{
|
||||
Customer = paymentMethod.CustomerId,
|
||||
Status = StripeSubscriptionStatus.Unpaid,
|
||||
Expand = new List<string> { "data.latest_invoice" }
|
||||
Expand = ["data.latest_invoice"]
|
||||
};
|
||||
|
||||
StripeList<Subscription> unpaidSubscriptions;
|
||||
@ -672,9 +673,9 @@ public class StripeController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private Tuple<Guid?, Guid?> GetIdsFromMetaData(IDictionary<string, string> metaData)
|
||||
private static Tuple<Guid?, Guid?> GetIdsFromMetaData(Dictionary<string, string> metaData)
|
||||
{
|
||||
if (metaData == null || !metaData.Any())
|
||||
if (metaData == null || metaData.Count == 0)
|
||||
{
|
||||
return new Tuple<Guid?, Guid?>(null, null);
|
||||
}
|
||||
@ -682,29 +683,35 @@ public class StripeController : Controller
|
||||
Guid? orgId = null;
|
||||
Guid? userId = null;
|
||||
|
||||
if (metaData.ContainsKey("organizationId"))
|
||||
if (metaData.TryGetValue("organizationId", out var orgIdString))
|
||||
{
|
||||
orgId = new Guid(metaData["organizationId"]);
|
||||
orgId = new Guid(orgIdString);
|
||||
}
|
||||
else if (metaData.ContainsKey("userId"))
|
||||
else if (metaData.TryGetValue("userId", out var userIdString))
|
||||
{
|
||||
userId = new Guid(metaData["userId"]);
|
||||
userId = new Guid(userIdString);
|
||||
}
|
||||
|
||||
if (userId == null && orgId == null)
|
||||
if (userId != null && userId != Guid.Empty || orgId != null && orgId != Guid.Empty)
|
||||
{
|
||||
var orgIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "organizationid");
|
||||
if (!string.IsNullOrWhiteSpace(orgIdKey))
|
||||
return new Tuple<Guid?, Guid?>(orgId, userId);
|
||||
}
|
||||
|
||||
var orgIdKey = metaData.Keys
|
||||
.FirstOrDefault(k => k.Equals("organizationid", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(orgIdKey))
|
||||
{
|
||||
orgId = new Guid(metaData[orgIdKey]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userIdKey = metaData.Keys
|
||||
.FirstOrDefault(k => k.Equals("userid", StringComparison.InvariantCultureIgnoreCase));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(userIdKey))
|
||||
{
|
||||
orgId = new Guid(metaData[orgIdKey]);
|
||||
}
|
||||
else
|
||||
{
|
||||
var userIdKey = metaData.Keys.FirstOrDefault(k => k.ToLowerInvariant() == "userid");
|
||||
if (!string.IsNullOrWhiteSpace(userIdKey))
|
||||
{
|
||||
userId = new Guid(metaData[userIdKey]);
|
||||
}
|
||||
userId = new Guid(metaData[userIdKey]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -891,9 +898,9 @@ public class StripeController : Controller
|
||||
return subscription;
|
||||
}
|
||||
|
||||
subscription.DefaultTaxRates = new List<Stripe.TaxRate> { stripeTaxRate };
|
||||
subscription.DefaultTaxRates = [stripeTaxRate];
|
||||
|
||||
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = new List<string> { stripeTaxRate.Id } };
|
||||
var subscriptionOptions = new SubscriptionUpdateOptions { DefaultTaxRates = [stripeTaxRate.Id] };
|
||||
subscription = await _stripeFacade.UpdateSubscription(subscription.Id, subscriptionOptions);
|
||||
|
||||
return subscription;
|
||||
|
@ -25,7 +25,10 @@ public class PayPalIPNTransactionModel
|
||||
|
||||
var data = queryString
|
||||
.AllKeys
|
||||
.ToDictionary(key => key, key => queryString[key]);
|
||||
.Where(key => !string.IsNullOrWhiteSpace(key))
|
||||
.ToDictionary(key =>
|
||||
key.Trim('\r'),
|
||||
key => queryString[key]?.Trim('\r'));
|
||||
|
||||
TransactionId = Extract(data, "txn_id");
|
||||
TransactionType = Extract(data, "txn_type");
|
||||
|
@ -0,0 +1,9 @@
|
||||
namespace Bit.Core.Billing.Constants;
|
||||
|
||||
public static class StripeCustomerAutomaticTaxStatus
|
||||
{
|
||||
public const string Failed = "failed";
|
||||
public const string NotCollecting = "not_collecting";
|
||||
public const string Supported = "supported";
|
||||
public const string UnrecognizedLocation = "unrecognized_location";
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -125,59 +126,61 @@ public class StripePaymentService : IPaymentService
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Stripe.Subscription subscription;
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: provider
|
||||
? ProviderDiscountId
|
||||
: null,
|
||||
Address = new Stripe.AddressOptions
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Country = taxInfo?.BillingAddressCountry,
|
||||
PostalCode = taxInfo?.BillingAddressPostalCode,
|
||||
// Line1 is required in Stripe's API, suggestion in Docs is to use Business Name instead.
|
||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
Line1 = taxInfo?.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo?.BillingAddressLine2,
|
||||
City = taxInfo?.BillingAddressCity,
|
||||
State = taxInfo?.BillingAddressState,
|
||||
},
|
||||
TaxIdData = !taxInfo.HasTaxId ? null : new List<Stripe.CustomerTaxIdDataOptions>
|
||||
{
|
||||
new Stripe.CustomerTaxIdDataOptions
|
||||
{
|
||||
Type = taxInfo.TaxIdType,
|
||||
Value = taxInfo.TaxIdNumber,
|
||||
},
|
||||
},
|
||||
});
|
||||
TaxIdData = taxInfo?.HasTaxId != true
|
||||
? null
|
||||
:
|
||||
[
|
||||
new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber, }
|
||||
],
|
||||
};
|
||||
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
subCreateOptions.Customer = customer.Id;
|
||||
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
subCreateOptions.AutomaticTax = new Stripe.SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
@ -185,7 +188,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
@ -252,9 +255,10 @@ public class StripePaymentService : IPaymentService
|
||||
throw new BadRequestException("Organization already has a subscription.");
|
||||
}
|
||||
|
||||
var customerOptions = new Stripe.CustomerGetOptions();
|
||||
var customerOptions = new CustomerGetOptions();
|
||||
customerOptions.AddExpand("default_source");
|
||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerOptions.AddExpand("tax");
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId, customerOptions);
|
||||
if (customer == null)
|
||||
{
|
||||
@ -301,14 +305,15 @@ public class StripePaymentService : IPaymentService
|
||||
var customerUpdateOptions = new CustomerUpdateOptions { Address = addressOptions };
|
||||
customerUpdateOptions.AddExpand("default_source");
|
||||
customerUpdateOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
customerUpdateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerUpdateAsync(org.GatewayCustomerId, customerUpdateOptions);
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, upgrade);
|
||||
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
subCreateOptions.DefaultTaxRates = new List<string>();
|
||||
subCreateOptions.DefaultTaxRates = [];
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
@ -333,7 +338,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
||||
Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions)
|
||||
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
||||
{
|
||||
var stripePaymentMethod = false;
|
||||
var paymentMethodType = PaymentMethodType.Credit;
|
||||
@ -351,12 +356,12 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else if (customer.DefaultSource != null)
|
||||
{
|
||||
if (customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.SourceCard)
|
||||
if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
else if (customer.DefaultSource is Stripe.BankAccount || customer.DefaultSource is Stripe.SourceAchDebit)
|
||||
else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.BankAccount;
|
||||
stripePaymentMethod = true;
|
||||
@ -394,7 +399,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var createdStripeCustomer = false;
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
||||
or PaymentMethodType.Credit;
|
||||
@ -422,14 +427,23 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
try
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId);
|
||||
var customerGetOptions = new CustomerGetOptions();
|
||||
customerGetOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerGetAsync(user.GatewayCustomerId, customerGetOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Attempted to get existing customer from Stripe, but customer ID was not found. Attempting to recreate customer...");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
var stripeCustomerMetadata = new Dictionary<string, string> { { "region", _globalSettings.BaseServiceUri.CloudRegion } };
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
@ -458,32 +472,35 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = user.Name,
|
||||
Email = user.Email,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Source = stipeCustomerSourceToken,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = user.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = new Stripe.AddressOptions
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = string.Empty,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
},
|
||||
});
|
||||
};
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
createdStripeCustomer = true;
|
||||
}
|
||||
|
||||
@ -492,17 +509,17 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Could not set up customer payment profile.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new Stripe.SubscriptionCreateOptions
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Items = new List<Stripe.SubscriptionItemOptions>(),
|
||||
Items = [],
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[user.GatewayIdField()] = user.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = PremiumPlanId,
|
||||
Quantity = 1
|
||||
@ -524,25 +541,22 @@ public class StripePaymentService : IPaymentService
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null)
|
||||
{
|
||||
subCreateOptions.DefaultTaxRates = new List<string>(1)
|
||||
{
|
||||
taxRate.Id
|
||||
};
|
||||
subCreateOptions.DefaultTaxRates = [taxRate.Id];
|
||||
}
|
||||
}
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new Stripe.SubscriptionItemOptions
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = StoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
subCreateOptions.DefaultTaxRates = new List<string>();
|
||||
subCreateOptions.DefaultTaxRates = [];
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
@ -558,34 +572,33 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Stripe.Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Stripe.Customer customer,
|
||||
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
|
||||
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
||||
Stripe.SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
{
|
||||
var addedCreditToStripeCustomer = false;
|
||||
Braintree.Transaction braintreeTransaction = null;
|
||||
|
||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||
Stripe.Subscription subscription = null;
|
||||
Subscription subscription = null;
|
||||
try
|
||||
{
|
||||
if (!stripePaymentMethod)
|
||||
{
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
||||
});
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
|
||||
CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
||||
}
|
||||
@ -632,7 +645,7 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("No payment was able to be collected.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance - previewInvoice.AmountDue
|
||||
});
|
||||
@ -649,10 +662,10 @@ public class StripePaymentService : IPaymentService
|
||||
};
|
||||
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled && CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
upcomingInvoiceOptions.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true };
|
||||
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = new List<string>();
|
||||
upcomingInvoiceOptions.SubscriptionDefaultTaxRates = [];
|
||||
}
|
||||
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
||||
@ -666,17 +679,12 @@ public class StripePaymentService : IPaymentService
|
||||
subCreateOptions.OffSession = true;
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax))
|
||||
{
|
||||
subCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
if (subscription.Status == "incomplete" && subscription.LatestInvoice?.PaymentIntent != null)
|
||||
{
|
||||
if (subscription.LatestInvoice.PaymentIntent.Status == "requires_payment_method")
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new Stripe.SubscriptionCancelOptions());
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscription.Id, new SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
@ -694,7 +702,7 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Invoice not found.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = subInvoiceMetadata
|
||||
});
|
||||
@ -712,7 +720,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance
|
||||
});
|
||||
@ -727,7 +735,7 @@ public class StripePaymentService : IPaymentService
|
||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||
}
|
||||
|
||||
if (e is Stripe.StripeException strEx &&
|
||||
if (e is StripeException strEx &&
|
||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
@ -737,10 +745,10 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
private List<Stripe.InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<Stripe.SubscriptionItemOptions> subItemOptions)
|
||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<SubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions
|
||||
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
||||
{
|
||||
Plan = si.Plan,
|
||||
Price = si.Price,
|
||||
@ -753,7 +761,10 @@ public class StripePaymentService : IPaymentService
|
||||
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
|
||||
{
|
||||
// remember, when in doubt, throw
|
||||
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
|
||||
var subGetOptions = new SubscriptionGetOptions();
|
||||
// subGetOptions.AddExpand("customer");
|
||||
subGetOptions.AddExpand("customer.tax");
|
||||
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId, subGetOptions);
|
||||
if (sub == null)
|
||||
{
|
||||
throw new GatewayException("Subscription not found.");
|
||||
@ -766,7 +777,7 @@ public class StripePaymentService : IPaymentService
|
||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||
|
||||
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
|
||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = updatedItemOptions,
|
||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
||||
@ -797,9 +808,11 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
|
||||
if (pm5766AutomaticTaxIsEnabled)
|
||||
if (pm5766AutomaticTaxIsEnabled &&
|
||||
sub.AutomaticTax.Enabled != true &&
|
||||
CustomerHasTaxLocationVerified(sub.Customer))
|
||||
{
|
||||
subUpdateOptions.DefaultTaxRates = new List<string>();
|
||||
subUpdateOptions.DefaultTaxRates = [];
|
||||
subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
|
||||
}
|
||||
|
||||
@ -824,7 +837,7 @@ public class StripePaymentService : IPaymentService
|
||||
var taxRate = taxRates.FirstOrDefault();
|
||||
if (taxRate != null && !sub.DefaultTaxRates.Any(x => x.Equals(taxRate.Id)))
|
||||
{
|
||||
subUpdateOptions.DefaultTaxRates = new List<string>(1) { taxRate.Id };
|
||||
subUpdateOptions.DefaultTaxRates = [taxRate.Id];
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -834,7 +847,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions);
|
||||
|
||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions());
|
||||
var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new InvoiceGetOptions());
|
||||
if (invoice == null)
|
||||
{
|
||||
throw new BadRequestException("Unable to locate draft invoice for subscription update.");
|
||||
@ -852,11 +865,11 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
});
|
||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
|
||||
await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new InvoiceSendOptions());
|
||||
paymentIntentClientSecret = null;
|
||||
}
|
||||
}
|
||||
@ -864,7 +877,7 @@ public class StripePaymentService : IPaymentService
|
||||
catch
|
||||
{
|
||||
// Need to revert the subscription
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = subscriptionUpdate.RevertItemsOptions(sub),
|
||||
// This proration behavior prevents a false "credit" from
|
||||
@ -889,7 +902,7 @@ public class StripePaymentService : IPaymentService
|
||||
// Change back the subscription collection method and/or days until due
|
||||
if (collectionMethod != "send_invoice" || daysUntilDue == null)
|
||||
{
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new SubscriptionUpdateOptions
|
||||
{
|
||||
CollectionMethod = collectionMethod,
|
||||
DaysUntilDue = daysUntilDue,
|
||||
@ -950,7 +963,7 @@ public class StripePaymentService : IPaymentService
|
||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewaySubscriptionId))
|
||||
{
|
||||
await _stripeAdapter.SubscriptionCancelAsync(subscriber.GatewaySubscriptionId,
|
||||
new Stripe.SubscriptionCancelOptions());
|
||||
new SubscriptionCancelOptions());
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
@ -983,7 +996,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
var charges = await _stripeAdapter.ChargeListAsync(new Stripe.ChargeListOptions
|
||||
var charges = await _stripeAdapter.ChargeListAsync(new ChargeListOptions
|
||||
{
|
||||
Customer = subscriber.GatewayCustomerId
|
||||
});
|
||||
@ -992,7 +1005,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
foreach (var charge in charges.Data.Where(c => c.Captured && !c.Refunded))
|
||||
{
|
||||
await _stripeAdapter.RefundCreateAsync(new Stripe.RefundCreateOptions { Charge = charge.Id });
|
||||
await _stripeAdapter.RefundCreateAsync(new RefundCreateOptions { Charge = charge.Id });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1000,9 +1013,9 @@ public class StripePaymentService : IPaymentService
|
||||
await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId);
|
||||
}
|
||||
|
||||
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice)
|
||||
public async Task<string> PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Invoice invoice)
|
||||
{
|
||||
var customerOptions = new Stripe.CustomerGetOptions();
|
||||
var customerOptions = new CustomerGetOptions();
|
||||
customerOptions.AddExpand("default_source");
|
||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions);
|
||||
@ -1016,7 +1029,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card";
|
||||
var hasDefaultValidSource = customer.DefaultSource != null &&
|
||||
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount);
|
||||
(customer.DefaultSource is Card || customer.DefaultSource is BankAccount);
|
||||
if (!hasDefaultCardPaymentMethod && !hasDefaultValidSource)
|
||||
{
|
||||
cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id;
|
||||
@ -1029,7 +1042,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
catch
|
||||
{
|
||||
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
||||
await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false
|
||||
});
|
||||
@ -1045,11 +1058,11 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
// Finalize the invoice (from Draft) w/o auto-advance so we
|
||||
// can attempt payment manually.
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new Stripe.InvoiceFinalizeOptions
|
||||
invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(invoice.Id, new InvoiceFinalizeOptions
|
||||
{
|
||||
AutoAdvance = false,
|
||||
});
|
||||
var invoicePayOptions = new Stripe.InvoicePayOptions
|
||||
var invoicePayOptions = new InvoicePayOptions
|
||||
{
|
||||
PaymentMethod = cardPaymentMethodId,
|
||||
};
|
||||
@ -1083,7 +1096,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
braintreeTransaction = transactionResult.Target;
|
||||
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions
|
||||
invoice = await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
@ -1099,13 +1112,13 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
invoice = await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions);
|
||||
}
|
||||
catch (Stripe.StripeException e)
|
||||
catch (StripeException e)
|
||||
{
|
||||
if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired &&
|
||||
e.StripeError?.Code == "invoice_payment_intent_requires_action")
|
||||
{
|
||||
// SCA required, get intent client secret
|
||||
var invoiceGetOptions = new Stripe.InvoiceGetOptions();
|
||||
var invoiceGetOptions = new InvoiceGetOptions();
|
||||
invoiceGetOptions.AddExpand("payment_intent");
|
||||
invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions);
|
||||
paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret;
|
||||
@ -1130,7 +1143,7 @@ public class StripePaymentService : IPaymentService
|
||||
return paymentIntentClientSecret;
|
||||
}
|
||||
|
||||
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions());
|
||||
invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new InvoiceVoidOptions());
|
||||
|
||||
// HACK: Workaround for customer balance credit
|
||||
if (invoice.StartingBalance < 0)
|
||||
@ -1143,7 +1156,7 @@ public class StripePaymentService : IPaymentService
|
||||
// Assumption: Customer balance should now be $0, otherwise payment would not have failed.
|
||||
if (customer.Balance == 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = invoice.StartingBalance
|
||||
});
|
||||
@ -1151,7 +1164,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
}
|
||||
|
||||
if (e is Stripe.StripeException strEx &&
|
||||
if (e is StripeException strEx &&
|
||||
(strEx.StripeError?.Message?.Contains("cannot be used because it is not verified") ?? false))
|
||||
{
|
||||
throw new GatewayException("Bank account is not yet verified.");
|
||||
@ -1192,14 +1205,14 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
var canceledSub = endOfPeriod ?
|
||||
await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
||||
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new Stripe.SubscriptionCancelOptions());
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = true }) :
|
||||
await _stripeAdapter.SubscriptionCancelAsync(sub.Id, new SubscriptionCancelOptions());
|
||||
if (!canceledSub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new GatewayException("Unable to cancel subscription.");
|
||||
}
|
||||
}
|
||||
catch (Stripe.StripeException e)
|
||||
catch (StripeException e)
|
||||
{
|
||||
if (e.Message != $"No such subscription: {subscriber.GatewaySubscriptionId}")
|
||||
{
|
||||
@ -1233,7 +1246,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var updatedSub = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id,
|
||||
new Stripe.SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
|
||||
if (updatedSub.CanceledAt.HasValue)
|
||||
{
|
||||
throw new GatewayException("Unable to reinstate subscription.");
|
||||
@ -1264,12 +1277,11 @@ public class StripePaymentService : IPaymentService
|
||||
};
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount;
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
{
|
||||
var options = new Stripe.CustomerGetOptions();
|
||||
options.AddExpand("sources");
|
||||
var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] };
|
||||
customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options);
|
||||
if (customer.Metadata?.Any() ?? false)
|
||||
{
|
||||
@ -1369,26 +1381,27 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||
{
|
||||
Description = subscriber.BillingName(),
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
Metadata = stripeCustomerMetadata,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
||||
Address = taxInfo == null ? null : new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
@ -1397,7 +1410,7 @@ public class StripePaymentService : IPaymentService
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
},
|
||||
Expand = new List<string> { "sources" },
|
||||
Expand = ["sources", "tax", "subscriptions"],
|
||||
});
|
||||
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
@ -1413,7 +1426,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_"))
|
||||
{
|
||||
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new Stripe.BankAccountCreateOptions
|
||||
var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions
|
||||
{
|
||||
Source = paymentToken
|
||||
});
|
||||
@ -1422,7 +1435,7 @@ public class StripePaymentService : IPaymentService
|
||||
else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId))
|
||||
{
|
||||
await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId,
|
||||
new Stripe.PaymentMethodAttachOptions { Customer = customer.Id });
|
||||
new PaymentMethodAttachOptions { Customer = customer.Id });
|
||||
defaultPaymentMethodId = stipeCustomerPaymentMethodId;
|
||||
}
|
||||
}
|
||||
@ -1431,44 +1444,44 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId))
|
||||
{
|
||||
if (source is Stripe.BankAccount)
|
||||
if (source is BankAccount)
|
||||
{
|
||||
await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id);
|
||||
}
|
||||
else if (source is Stripe.Card)
|
||||
else if (source is Card)
|
||||
{
|
||||
await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new Stripe.PaymentMethodListOptions
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Type = "card"
|
||||
});
|
||||
foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId))
|
||||
{
|
||||
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new Stripe.PaymentMethodDetachOptions());
|
||||
await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions());
|
||||
}
|
||||
|
||||
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Metadata = stripeCustomerMetadata,
|
||||
DefaultSource = defaultSourceId,
|
||||
InvoiceSettings = new Stripe.CustomerInvoiceSettingsOptions
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = defaultPaymentMethodId,
|
||||
CustomFields = new List<Stripe.CustomerInvoiceSettingsCustomFieldOptions>
|
||||
{
|
||||
new Stripe.CustomerInvoiceSettingsCustomFieldOptions()
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
||||
},
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
Address = taxInfo == null ? null : new Stripe.AddressOptions
|
||||
Address = taxInfo == null ? null : new AddressOptions
|
||||
{
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
@ -1477,8 +1490,27 @@ public class StripePaymentService : IPaymentService
|
||||
City = taxInfo.BillingAddressCity,
|
||||
State = taxInfo.BillingAddressState,
|
||||
},
|
||||
Expand = ["tax", "subscriptions"]
|
||||
});
|
||||
}
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax) &&
|
||||
!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) &&
|
||||
customer.Subscriptions.Any(sub =>
|
||||
sub.Id == subscriber.GatewaySubscriptionId &&
|
||||
!sub.AutomaticTax.Enabled) &&
|
||||
CustomerHasTaxLocationVerified(customer))
|
||||
{
|
||||
var subscriptionUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true },
|
||||
DefaultTaxRates = []
|
||||
};
|
||||
|
||||
_ = await _stripeAdapter.SubscriptionUpdateAsync(
|
||||
subscriber.GatewaySubscriptionId,
|
||||
subscriptionUpdateOptions);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@ -1494,7 +1526,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
public async Task<bool> CreditAccountAsync(ISubscriber subscriber, decimal creditAmount)
|
||||
{
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
var customerExists = subscriber.Gateway == GatewayType.Stripe &&
|
||||
!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId);
|
||||
if (customerExists)
|
||||
@ -1503,7 +1535,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
else
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new Stripe.CustomerCreateOptions
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions
|
||||
{
|
||||
Email = subscriber.BillingEmailAddress(),
|
||||
Description = subscriber.BillingName(),
|
||||
@ -1511,7 +1543,7 @@ public class StripePaymentService : IPaymentService
|
||||
subscriber.Gateway = GatewayType.Stripe;
|
||||
subscriber.GatewayCustomerId = customer.Id;
|
||||
}
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new Stripe.CustomerUpdateOptions
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance - (long)(creditAmount * 100)
|
||||
});
|
||||
@ -1614,7 +1646,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId,
|
||||
new Stripe.CustomerGetOptions { Expand = new List<string> { "tax_ids" } });
|
||||
new CustomerGetOptions { Expand = ["tax_ids"] });
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
@ -1647,9 +1679,9 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
if (subscriber != null && !string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId))
|
||||
{
|
||||
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new Stripe.CustomerUpdateOptions
|
||||
var customer = await _stripeAdapter.CustomerUpdateAsync(subscriber.GatewayCustomerId, new CustomerUpdateOptions
|
||||
{
|
||||
Address = new Stripe.AddressOptions
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = taxInfo.BillingAddressLine1 ?? string.Empty,
|
||||
Line2 = taxInfo.BillingAddressLine2,
|
||||
@ -1658,7 +1690,7 @@ public class StripePaymentService : IPaymentService
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
},
|
||||
Expand = new List<string> { "tax_ids" }
|
||||
Expand = ["tax_ids"]
|
||||
});
|
||||
|
||||
if (!subscriber.IsUser() && customer != null)
|
||||
@ -1672,7 +1704,7 @@ public class StripePaymentService : IPaymentService
|
||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) &&
|
||||
!string.IsNullOrWhiteSpace(taxInfo.TaxIdType))
|
||||
{
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new Stripe.TaxIdCreateOptions
|
||||
await _stripeAdapter.TaxIdCreateAsync(customer.Id, new TaxIdCreateOptions
|
||||
{
|
||||
Type = taxInfo.TaxIdType,
|
||||
Value = taxInfo.TaxIdNumber,
|
||||
@ -1684,7 +1716,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
public async Task<TaxRate> CreateTaxRateAsync(TaxRate taxRate)
|
||||
{
|
||||
var stripeTaxRateOptions = new Stripe.TaxRateCreateOptions()
|
||||
var stripeTaxRateOptions = new TaxRateCreateOptions()
|
||||
{
|
||||
DisplayName = $"{taxRate.Country} - {taxRate.PostalCode}",
|
||||
Inclusive = false,
|
||||
@ -1717,7 +1749,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
var updatedStripeTaxRate = await _stripeAdapter.TaxRateUpdateAsync(
|
||||
taxRate.Id,
|
||||
new Stripe.TaxRateUpdateOptions() { Active = false }
|
||||
new TaxRateUpdateOptions() { Active = false }
|
||||
);
|
||||
if (!updatedStripeTaxRate.Active)
|
||||
{
|
||||
@ -1755,19 +1787,19 @@ public class StripePaymentService : IPaymentService
|
||||
return paymentSource == null;
|
||||
}
|
||||
|
||||
private Stripe.PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
private PaymentMethod GetLatestCardPaymentMethod(string customerId)
|
||||
{
|
||||
var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(
|
||||
new Stripe.PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
||||
new PaymentMethodListOptions { Customer = customerId, Type = "card" });
|
||||
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
||||
}
|
||||
|
||||
private decimal GetBillingBalance(Stripe.Customer customer)
|
||||
private decimal GetBillingBalance(Customer customer)
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Stripe.Customer customer)
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
@ -1796,7 +1828,7 @@ public class StripePaymentService : IPaymentService
|
||||
}
|
||||
|
||||
if (customer.DefaultSource != null &&
|
||||
(customer.DefaultSource is Stripe.Card || customer.DefaultSource is Stripe.BankAccount))
|
||||
(customer.DefaultSource is Card || customer.DefaultSource is BankAccount))
|
||||
{
|
||||
return new BillingInfo.BillingSource(customer.DefaultSource);
|
||||
}
|
||||
@ -1805,27 +1837,27 @@ public class StripePaymentService : IPaymentService
|
||||
return paymentMethod != null ? new BillingInfo.BillingSource(paymentMethod) : null;
|
||||
}
|
||||
|
||||
private Stripe.CustomerGetOptions GetCustomerPaymentOptions()
|
||||
private CustomerGetOptions GetCustomerPaymentOptions()
|
||||
{
|
||||
var customerOptions = new Stripe.CustomerGetOptions();
|
||||
var customerOptions = new CustomerGetOptions();
|
||||
customerOptions.AddExpand("default_source");
|
||||
customerOptions.AddExpand("invoice_settings.default_payment_method");
|
||||
return customerOptions;
|
||||
}
|
||||
|
||||
private async Task<Stripe.Customer> GetCustomerAsync(string gatewayCustomerId, Stripe.CustomerGetOptions options = null)
|
||||
private async Task<Customer> GetCustomerAsync(string gatewayCustomerId, CustomerGetOptions options = null)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(gatewayCustomerId))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
Stripe.Customer customer = null;
|
||||
Customer customer = null;
|
||||
try
|
||||
{
|
||||
customer = await _stripeAdapter.CustomerGetAsync(gatewayCustomerId, options);
|
||||
}
|
||||
catch (Stripe.StripeException) { }
|
||||
catch (StripeException) { }
|
||||
|
||||
return customer;
|
||||
}
|
||||
@ -1847,7 +1879,7 @@ public class StripePaymentService : IPaymentService
|
||||
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Stripe.Customer customer)
|
||||
private async Task<IEnumerable<BillingInfo.BillingInvoice>> GetBillingInvoicesAsync(Customer customer)
|
||||
{
|
||||
if (customer == null)
|
||||
{
|
||||
@ -1869,28 +1901,32 @@ public class StripePaymentService : IPaymentService
|
||||
.OrderByDescending(invoice => invoice.Created)
|
||||
.Select(invoice => new BillingInfo.BillingInvoice(invoice));
|
||||
}
|
||||
catch (Stripe.StripeException exception)
|
||||
catch (StripeException exception)
|
||||
{
|
||||
_logger.LogError(exception, "An error occurred while listing Stripe invoices");
|
||||
throw new GatewayException("Failed to retrieve current invoices", exception);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines if a Stripe customer supports automatic tax
|
||||
/// </summary>
|
||||
/// <param name="customer"></param>
|
||||
/// <returns></returns>
|
||||
private static bool CustomerHasTaxLocationVerified(Customer customer) =>
|
||||
customer?.Tax?.AutomaticTax == StripeCustomerAutomaticTaxStatus.Supported;
|
||||
|
||||
// We are taking only first 30 characters of the SubscriberName because stripe provide
|
||||
// for 30 characters for custom_fields,see the link: https://stripe.com/docs/api/invoices/create
|
||||
public static string GetFirstThirtyCharacters(string subscriberName)
|
||||
private static string GetFirstThirtyCharacters(string subscriberName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
else if (subscriberName.Length <= 30)
|
||||
{
|
||||
return subscriberName;
|
||||
}
|
||||
else
|
||||
{
|
||||
return subscriberName.Substring(0, 30);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return subscriberName.Length <= 30
|
||||
? subscriberName
|
||||
: subscriberName[..30];
|
||||
}
|
||||
}
|
||||
|
@ -487,7 +487,7 @@ public class PayPalControllerTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostIpn_Refunded_MissingParentTransaction_BadRequest()
|
||||
public async Task PostIpn_Refunded_MissingParentTransaction_Ok()
|
||||
{
|
||||
var logger = _testOutputHelper.BuildLoggerFor<PayPalController>();
|
||||
|
||||
@ -518,9 +518,9 @@ public class PayPalControllerTests
|
||||
|
||||
var result = await controller.PostIpn();
|
||||
|
||||
HasStatusCode(result, 400);
|
||||
HasStatusCode(result, 200);
|
||||
|
||||
LoggedError(logger, "PayPal IPN (2PK15573S8089712Y): Could not find parent transaction");
|
||||
LoggedWarning(logger, "PayPal IPN (2PK15573S8089712Y): Could not find parent transaction");
|
||||
|
||||
await _transactionRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(Arg.Any<Transaction>());
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user