mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 07:36:14 -05:00
[BEEEP] [PM-18518] Cleanup StripePaymentService (#5435)
This commit is contained in:
@ -22,4 +22,9 @@ public static class CustomerExtensions
|
||||
/// <returns></returns>
|
||||
public static bool HasTaxLocationVerified(this Customer customer) =>
|
||||
customer?.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported;
|
||||
|
||||
public static decimal GetBillingBalance(this Customer customer)
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
}
|
||||
|
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
26
src/Core/Billing/Extensions/SubscriberExtensions.cs
Normal file
@ -0,0 +1,26 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.Billing.Extensions;
|
||||
|
||||
public static class SubscriberExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
/// <param name="subscriber"></param>
|
||||
/// <returns></returns>
|
||||
public static string GetFormattedInvoiceName(this ISubscriber subscriber)
|
||||
{
|
||||
var subscriberName = subscriber.SubscriberName();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return subscriberName.Length <= 30
|
||||
? subscriberName
|
||||
: subscriberName[..30];
|
||||
}
|
||||
}
|
@ -14,18 +14,8 @@ namespace Bit.Core.Services;
|
||||
public interface IPaymentService
|
||||
{
|
||||
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
|
||||
Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats,
|
||||
bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false, int additionalSmSeats = 0,
|
||||
int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false);
|
||||
Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, Plan plan, int additionalSeats,
|
||||
bool premiumAccessAddon, int additionalSmSeats = 0, int additionalServiceAccount = 0,
|
||||
bool signupIsFromSecretsManagerTrial = false);
|
||||
Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship);
|
||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||
short additionalStorageGb, TaxInfo taxInfo);
|
||||
Task<string> AdjustSubscription(
|
||||
Organization organization,
|
||||
Plan updatedPlan,
|
||||
@ -56,9 +46,7 @@ public interface IPaymentService
|
||||
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
|
||||
Task<string> AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
|
||||
int additionalServiceAccount);
|
||||
Task<bool> RisksSubscriptionFailure(Organization organization);
|
||||
Task<bool> HasSecretsManagerStandalone(Organization organization);
|
||||
Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
Task<PreviewInvoiceResponseModel> PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
|
||||
|
||||
|
@ -25,9 +25,6 @@ namespace Bit.Core.Services;
|
||||
|
||||
public class StripePaymentService : IPaymentService
|
||||
{
|
||||
private const string PremiumPlanId = "premium-annually";
|
||||
private const string StoragePlanId = "storage-gb-annually";
|
||||
private const string ProviderDiscountId = "msp-discount-35";
|
||||
private const string SecretsManagerStandaloneDiscountId = "sm-standalone";
|
||||
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
@ -62,240 +59,6 @@ public class StripePaymentService : IPaymentService
|
||||
_pricingClient = pricingClient;
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, StaticStore.Plan plan, short additionalStorageGb,
|
||||
int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo, bool provider = false,
|
||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
||||
{
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
string stipeCustomerSourceToken = null;
|
||||
string stipeCustomerPaymentMethodId = null;
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
var stripePaymentMethod = paymentMethodType == PaymentMethodType.Card ||
|
||||
paymentMethodType == PaymentMethodType.BankAccount;
|
||||
|
||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
if (paymentToken.StartsWith("pm_"))
|
||||
{
|
||||
stipeCustomerPaymentMethodId = paymentToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
stipeCustomerSourceToken = paymentToken;
|
||||
}
|
||||
}
|
||||
else if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
||||
{
|
||||
PaymentMethodNonce = paymentToken,
|
||||
Email = org.BillingEmail,
|
||||
Id = org.BraintreeCustomerIdPrefix() + org.Id.ToString("N").ToLower() + randomSuffix,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[org.BraintreeIdField()] = org.Id.ToString(),
|
||||
[org.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
||||
{
|
||||
throw new GatewayException("Failed to create PayPal customer record.");
|
||||
}
|
||||
|
||||
braintreeCustomer = customerResult.Target;
|
||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber))
|
||||
{
|
||||
taxInfo.TaxIdType = _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
|
||||
if (taxInfo.TaxIdType == null)
|
||||
{
|
||||
_logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
|
||||
taxInfo.BillingAddressCountry,
|
||||
taxInfo.TaxIdNumber);
|
||||
throw new BadRequestException("billingTaxIdTypeInferenceError");
|
||||
}
|
||||
}
|
||||
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Source = stipeCustomerSourceToken,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: provider
|
||||
? ProviderDiscountId
|
||||
: null,
|
||||
Address = new AddressOptions
|
||||
{
|
||||
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,
|
||||
},
|
||||
TaxIdData = !string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)
|
||||
? [new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }]
|
||||
: null
|
||||
};
|
||||
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
subCreateOptions.Customer = customer.Id;
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
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 SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
||||
if (customer != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
if (braintreeCustomer != null)
|
||||
{
|
||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
org.Gateway = GatewayType.Stripe;
|
||||
org.GatewayCustomerId = customer.Id;
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon,
|
||||
int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
|
||||
{
|
||||
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
var subCreateOptions = new OrganizationPurchaseSubscriptionOptions(org, plan, new TaxInfo(), additionalSeats, 0, premiumAccessAddon
|
||||
, additionalSmSeats, additionalServiceAccount);
|
||||
|
||||
Customer customer = null;
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = org.DisplayBusinessName(),
|
||||
Email = org.BillingEmail,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions
|
||||
{
|
||||
Name = org.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(org.SubscriberName()),
|
||||
}
|
||||
],
|
||||
},
|
||||
Coupon = signupIsFromSecretsManagerTrial
|
||||
? SecretsManagerStandaloneDiscountId
|
||||
: null,
|
||||
TaxIdData = null,
|
||||
};
|
||||
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
subCreateOptions.Customer = customer.Id;
|
||||
|
||||
subscription = await _stripeAdapter.SubscriptionCreateAsync(subCreateOptions);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error creating customer, walking back operation.");
|
||||
if (customer != null)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
|
||||
org.Gateway = GatewayType.Stripe;
|
||||
org.GatewayCustomerId = customer.Id;
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
private async Task ChangeOrganizationSponsorship(
|
||||
Organization org,
|
||||
OrganizationSponsorship sponsorship,
|
||||
@ -324,458 +87,6 @@ public class StripePaymentService : IPaymentService
|
||||
public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) =>
|
||||
ChangeOrganizationSponsorship(org, sponsorship, false);
|
||||
|
||||
public async Task<string> UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan,
|
||||
OrganizationUpgrade upgrade)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(org.GatewaySubscriptionId))
|
||||
{
|
||||
throw new BadRequestException("Organization already has a subscription.");
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
throw new GatewayException("Could not find customer payment profile.");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressCountry) &&
|
||||
!string.IsNullOrEmpty(upgrade.TaxInfo?.BillingAddressPostalCode))
|
||||
{
|
||||
var addressOptions = new AddressOptions
|
||||
{
|
||||
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 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);
|
||||
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions);
|
||||
|
||||
var subscription = await ChargeForNewSubscriptionAsync(org, customer, false,
|
||||
stripePaymentMethod, paymentMethodType, subCreateOptions, null);
|
||||
org.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
org.Enabled = false;
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
else
|
||||
{
|
||||
org.Enabled = true;
|
||||
org.ExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod(
|
||||
Customer customer, SubscriptionCreateOptions subCreateOptions)
|
||||
{
|
||||
var stripePaymentMethod = false;
|
||||
var paymentMethodType = PaymentMethodType.Credit;
|
||||
var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId");
|
||||
if (hasBtCustomerId)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.PayPal;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card")
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
else if (customer.DefaultSource != null)
|
||||
{
|
||||
if (customer.DefaultSource is Card || customer.DefaultSource is SourceCard)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
else if (customer.DefaultSource is BankAccount || customer.DefaultSource is SourceAchDebit)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.BankAccount;
|
||||
stripePaymentMethod = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var paymentMethod = GetLatestCardPaymentMethod(customer.Id);
|
||||
if (paymentMethod != null)
|
||||
{
|
||||
paymentMethodType = PaymentMethodType.Card;
|
||||
stripePaymentMethod = true;
|
||||
subCreateOptions.DefaultPaymentMethod = paymentMethod.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (stripePaymentMethod, paymentMethodType);
|
||||
}
|
||||
|
||||
public async Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType,
|
||||
string paymentToken, short additionalStorageGb, TaxInfo taxInfo)
|
||||
{
|
||||
if (paymentMethodType != PaymentMethodType.Credit && string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
throw new BadRequestException("Payment token is required.");
|
||||
}
|
||||
if (paymentMethodType == PaymentMethodType.Credit &&
|
||||
(user.Gateway != GatewayType.Stripe || string.IsNullOrWhiteSpace(user.GatewayCustomerId)))
|
||||
{
|
||||
throw new BadRequestException("Your account does not have any credit available.");
|
||||
}
|
||||
if (paymentMethodType is PaymentMethodType.BankAccount)
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var createdStripeCustomer = false;
|
||||
Customer customer = null;
|
||||
Braintree.Customer braintreeCustomer = null;
|
||||
var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount
|
||||
or PaymentMethodType.Credit;
|
||||
|
||||
string stipeCustomerPaymentMethodId = null;
|
||||
string stipeCustomerSourceToken = null;
|
||||
if (stripePaymentMethod && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
if (paymentToken.StartsWith("pm_"))
|
||||
{
|
||||
stipeCustomerPaymentMethodId = paymentToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
stipeCustomerSourceToken = paymentToken;
|
||||
}
|
||||
}
|
||||
|
||||
if (user.Gateway == GatewayType.Stripe && !string.IsNullOrWhiteSpace(user.GatewayCustomerId))
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
await UpdatePaymentMethodAsync(user, paymentMethodType, paymentToken, taxInfo);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
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...");
|
||||
}
|
||||
}
|
||||
|
||||
if (customer == null && !string.IsNullOrWhiteSpace(paymentToken))
|
||||
{
|
||||
var stripeCustomerMetadata = new Dictionary<string, string>
|
||||
{
|
||||
{ "region", _globalSettings.BaseServiceUri.CloudRegion }
|
||||
};
|
||||
if (paymentMethodType == PaymentMethodType.PayPal)
|
||||
{
|
||||
var randomSuffix = Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false);
|
||||
var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest
|
||||
{
|
||||
PaymentMethodNonce = paymentToken,
|
||||
Email = user.Email,
|
||||
Id = user.BraintreeCustomerIdPrefix() + user.Id.ToString("N").ToLower() + randomSuffix,
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[user.BraintreeIdField()] = user.Id.ToString(),
|
||||
[user.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0)
|
||||
{
|
||||
throw new GatewayException("Failed to create PayPal customer record.");
|
||||
}
|
||||
|
||||
braintreeCustomer = customerResult.Target;
|
||||
stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id);
|
||||
}
|
||||
else if (!stripePaymentMethod)
|
||||
{
|
||||
throw new GatewayException("Payment method is not supported at this time.");
|
||||
}
|
||||
|
||||
var customerCreateOptions = new CustomerCreateOptions
|
||||
{
|
||||
Description = user.Name,
|
||||
Email = user.Email,
|
||||
Metadata = stripeCustomerMetadata,
|
||||
PaymentMethod = stipeCustomerPaymentMethodId,
|
||||
Source = stipeCustomerSourceToken,
|
||||
InvoiceSettings = new CustomerInvoiceSettingsOptions
|
||||
{
|
||||
DefaultPaymentMethod = stipeCustomerPaymentMethodId,
|
||||
CustomFields =
|
||||
[
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = user.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(user.SubscriberName()),
|
||||
}
|
||||
|
||||
]
|
||||
},
|
||||
Address = new AddressOptions
|
||||
{
|
||||
Line1 = string.Empty,
|
||||
Country = taxInfo.BillingAddressCountry,
|
||||
PostalCode = taxInfo.BillingAddressPostalCode,
|
||||
},
|
||||
};
|
||||
customerCreateOptions.AddExpand("tax");
|
||||
customer = await _stripeAdapter.CustomerCreateAsync(customerCreateOptions);
|
||||
createdStripeCustomer = true;
|
||||
}
|
||||
|
||||
if (customer == null)
|
||||
{
|
||||
throw new GatewayException("Could not set up customer payment profile.");
|
||||
}
|
||||
|
||||
var subCreateOptions = new SubscriptionCreateOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
Items = [],
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[user.GatewayIdField()] = user.Id.ToString()
|
||||
}
|
||||
};
|
||||
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = PremiumPlanId,
|
||||
Quantity = 1
|
||||
});
|
||||
|
||||
if (additionalStorageGb > 0)
|
||||
{
|
||||
subCreateOptions.Items.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Plan = StoragePlanId,
|
||||
Quantity = additionalStorageGb
|
||||
});
|
||||
}
|
||||
|
||||
subCreateOptions.EnableAutomaticTax(customer);
|
||||
|
||||
var subscription = await ChargeForNewSubscriptionAsync(user, customer, createdStripeCustomer,
|
||||
stripePaymentMethod, paymentMethodType, subCreateOptions, braintreeCustomer);
|
||||
|
||||
user.Gateway = GatewayType.Stripe;
|
||||
user.GatewayCustomerId = customer.Id;
|
||||
user.GatewaySubscriptionId = subscription.Id;
|
||||
|
||||
if (subscription.Status == "incomplete" &&
|
||||
subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action")
|
||||
{
|
||||
return subscription.LatestInvoice.PaymentIntent.ClientSecret;
|
||||
}
|
||||
|
||||
user.Premium = true;
|
||||
user.PremiumExpirationDate = subscription.CurrentPeriodEnd;
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<Subscription> ChargeForNewSubscriptionAsync(ISubscriber subscriber, Customer customer,
|
||||
bool createdStripeCustomer, bool stripePaymentMethod, PaymentMethodType paymentMethodType,
|
||||
SubscriptionCreateOptions subCreateOptions, Braintree.Customer braintreeCustomer)
|
||||
{
|
||||
var addedCreditToStripeCustomer = false;
|
||||
Braintree.Transaction braintreeTransaction = null;
|
||||
|
||||
var subInvoiceMetadata = new Dictionary<string, string>();
|
||||
Subscription subscription = null;
|
||||
try
|
||||
{
|
||||
if (!stripePaymentMethod)
|
||||
{
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items)
|
||||
});
|
||||
|
||||
if (customer.HasTaxLocationVerified())
|
||||
{
|
||||
previewInvoice.AutomaticTax = new InvoiceAutomaticTax { Enabled = true };
|
||||
}
|
||||
|
||||
if (previewInvoice.AmountDue > 0)
|
||||
{
|
||||
var braintreeCustomerId = customer.Metadata != null &&
|
||||
customer.Metadata.ContainsKey("btCustomerId") ? customer.Metadata["btCustomerId"] : null;
|
||||
if (!string.IsNullOrWhiteSpace(braintreeCustomerId))
|
||||
{
|
||||
var btInvoiceAmount = (previewInvoice.AmountDue / 100M);
|
||||
var transactionResult = await _btGateway.Transaction.SaleAsync(
|
||||
new Braintree.TransactionRequest
|
||||
{
|
||||
Amount = btInvoiceAmount,
|
||||
CustomerId = braintreeCustomerId,
|
||||
Options = new Braintree.TransactionOptionsRequest
|
||||
{
|
||||
SubmitForSettlement = true,
|
||||
PayPal = new Braintree.TransactionOptionsPayPalRequest
|
||||
{
|
||||
CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id},{subscriber.BraintreeCloudRegionField()}:{_globalSettings.BaseServiceUri.CloudRegion}"
|
||||
}
|
||||
},
|
||||
CustomFields = new Dictionary<string, string>
|
||||
{
|
||||
[subscriber.BraintreeIdField()] = subscriber.Id.ToString(),
|
||||
[subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion
|
||||
}
|
||||
});
|
||||
|
||||
if (!transactionResult.IsSuccess())
|
||||
{
|
||||
throw new GatewayException("Failed to charge PayPal customer.");
|
||||
}
|
||||
|
||||
braintreeTransaction = transactionResult.Target;
|
||||
subInvoiceMetadata.Add("btTransactionId", braintreeTransaction.Id);
|
||||
subInvoiceMetadata.Add("btPayPalTransactionId",
|
||||
braintreeTransaction.PayPalDetails.AuthorizationId);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new GatewayException("No payment was able to be collected.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance - previewInvoice.AmountDue
|
||||
});
|
||||
addedCreditToStripeCustomer = true;
|
||||
}
|
||||
}
|
||||
else if (paymentMethodType == PaymentMethodType.Credit)
|
||||
{
|
||||
var upcomingInvoiceOptions = new UpcomingInvoiceOptions
|
||||
{
|
||||
Customer = customer.Id,
|
||||
SubscriptionItems = ToInvoiceSubscriptionItemOptions(subCreateOptions.Items),
|
||||
SubscriptionDefaultTaxRates = subCreateOptions.DefaultTaxRates,
|
||||
};
|
||||
|
||||
upcomingInvoiceOptions.EnableAutomaticTax(customer, null);
|
||||
|
||||
var previewInvoice = await _stripeAdapter.InvoiceUpcomingAsync(upcomingInvoiceOptions);
|
||||
|
||||
if (previewInvoice.AmountDue > 0)
|
||||
{
|
||||
throw new GatewayException("Your account does not have enough credit available.");
|
||||
}
|
||||
}
|
||||
|
||||
subCreateOptions.OffSession = true;
|
||||
subCreateOptions.AddExpand("latest_invoice.payment_intent");
|
||||
|
||||
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 SubscriptionCancelOptions());
|
||||
throw new GatewayException("Payment method was declined.");
|
||||
}
|
||||
}
|
||||
|
||||
if (!stripePaymentMethod && subInvoiceMetadata.Any())
|
||||
{
|
||||
var invoices = await _stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
|
||||
{
|
||||
Subscription = subscription.Id
|
||||
});
|
||||
|
||||
var invoice = invoices?.FirstOrDefault();
|
||||
if (invoice == null)
|
||||
{
|
||||
throw new GatewayException("Invoice not found.");
|
||||
}
|
||||
|
||||
await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new InvoiceUpdateOptions
|
||||
{
|
||||
Metadata = subInvoiceMetadata
|
||||
});
|
||||
}
|
||||
|
||||
return subscription;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (customer != null)
|
||||
{
|
||||
if (createdStripeCustomer)
|
||||
{
|
||||
await _stripeAdapter.CustomerDeleteAsync(customer.Id);
|
||||
}
|
||||
else if (addedCreditToStripeCustomer || customer.Balance < 0)
|
||||
{
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
Balance = customer.Balance
|
||||
});
|
||||
}
|
||||
}
|
||||
if (braintreeTransaction != null)
|
||||
{
|
||||
await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id);
|
||||
}
|
||||
if (braintreeCustomer != null)
|
||||
{
|
||||
await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id);
|
||||
}
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private List<InvoiceSubscriptionItemOptions> ToInvoiceSubscriptionItemOptions(
|
||||
List<SubscriptionItemOptions> subItemOptions)
|
||||
{
|
||||
return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
|
||||
{
|
||||
Plan = si.Plan,
|
||||
Price = si.Price,
|
||||
Quantity = si.Quantity,
|
||||
Id = si.Id
|
||||
}).ToList();
|
||||
}
|
||||
|
||||
private async Task<string> FinalizeSubscriptionChangeAsync(ISubscriber subscriber,
|
||||
SubscriptionUpdate subscriptionUpdate, bool invoiceNow = false)
|
||||
{
|
||||
@ -1400,7 +711,7 @@ public class StripePaymentService : IPaymentService
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName()),
|
||||
Value = subscriber.GetFormattedInvoiceName()
|
||||
}
|
||||
|
||||
]
|
||||
@ -1492,7 +803,7 @@ public class StripePaymentService : IPaymentService
|
||||
new CustomerInvoiceSettingsCustomFieldOptions()
|
||||
{
|
||||
Name = subscriber.SubscriberType(),
|
||||
Value = GetFirstThirtyCharacters(subscriber.SubscriberName())
|
||||
Value = subscriber.GetFormattedInvoiceName()
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -1560,7 +871,7 @@ public class StripePaymentService : IPaymentService
|
||||
var customer = await GetCustomerAsync(subscriber.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
var billingInfo = new BillingInfo
|
||||
{
|
||||
Balance = GetBillingBalance(customer),
|
||||
Balance = customer.GetBillingBalance(),
|
||||
PaymentSource = await GetBillingPaymentSourceAsync(customer)
|
||||
};
|
||||
|
||||
@ -1768,27 +1079,6 @@ public class StripePaymentService : IPaymentService
|
||||
new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount),
|
||||
true);
|
||||
|
||||
public async Task<bool> RisksSubscriptionFailure(Organization organization)
|
||||
{
|
||||
var subscriptionInfo = await GetSubscriptionAsync(organization);
|
||||
|
||||
if (subscriptionInfo.Subscription is not
|
||||
{
|
||||
Status: "active" or "trialing" or "past_due",
|
||||
CollectionMethod: "charge_automatically"
|
||||
}
|
||||
|| subscriptionInfo.UpcomingInvoice == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var customer = await GetCustomerAsync(organization.GatewayCustomerId, GetCustomerPaymentOptions());
|
||||
|
||||
var paymentSource = await GetBillingPaymentSourceAsync(customer);
|
||||
|
||||
return paymentSource == null;
|
||||
}
|
||||
|
||||
public async Task<bool> HasSecretsManagerStandalone(Organization organization)
|
||||
{
|
||||
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
@ -1801,7 +1091,7 @@ public class StripePaymentService : IPaymentService
|
||||
return customer?.Discount?.Coupon?.Id == SecretsManagerStandaloneDiscountId;
|
||||
}
|
||||
|
||||
public async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||
private async Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Subscription subscription)
|
||||
{
|
||||
if (subscription.Status is not "past_due" && subscription.Status is not "unpaid")
|
||||
{
|
||||
@ -2117,11 +1407,6 @@ public class StripePaymentService : IPaymentService
|
||||
return cardPaymentMethods.OrderByDescending(m => m.Created).FirstOrDefault();
|
||||
}
|
||||
|
||||
private decimal GetBillingBalance(Customer customer)
|
||||
{
|
||||
return customer != null ? customer.Balance / 100M : default;
|
||||
}
|
||||
|
||||
private async Task<BillingInfo.BillingSource> GetBillingPaymentSourceAsync(Customer customer)
|
||||
{
|
||||
if (customer == null)
|
||||
@ -2252,18 +1537,4 @@ public class StripePaymentService : IPaymentService
|
||||
throw new GatewayException("Failed to retrieve current invoices", exception);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
private static string GetFirstThirtyCharacters(string subscriberName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(subscriberName))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return subscriberName.Length <= 30
|
||||
? subscriberName
|
||||
: subscriberName[..30];
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user