1
0
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:
Jonas Hendrickx
2025-03-07 09:52:04 +01:00
committed by GitHub
parent 6cb97d9bf9
commit c589f9a330
6 changed files with 58 additions and 1573 deletions

View File

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

View 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];
}
}

View File

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

View File

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