diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs
index 1847abb0ad..1ab595342e 100644
--- a/src/Core/Billing/Extensions/CustomerExtensions.cs
+++ b/src/Core/Billing/Extensions/CustomerExtensions.cs
@@ -22,4 +22,9 @@ public static class CustomerExtensions
///
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;
+ }
}
diff --git a/src/Core/Billing/Extensions/SubscriberExtensions.cs b/src/Core/Billing/Extensions/SubscriberExtensions.cs
new file mode 100644
index 0000000000..e322ed7317
--- /dev/null
+++ b/src/Core/Billing/Extensions/SubscriberExtensions.cs
@@ -0,0 +1,26 @@
+using Bit.Core.Entities;
+
+namespace Bit.Core.Billing.Extensions;
+
+public static class SubscriberExtensions
+{
+ ///
+ /// 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 GetFormattedInvoiceName(this ISubscriber subscriber)
+ {
+ var subscriberName = subscriber.SubscriberName();
+
+ if (string.IsNullOrWhiteSpace(subscriberName))
+ {
+ return string.Empty;
+ }
+
+ return subscriberName.Length <= 30
+ ? subscriberName
+ : subscriberName[..30];
+ }
+}
diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs
index 5bd2bede33..e3495c0e65 100644
--- a/src/Core/Services/IPaymentService.cs
+++ b/src/Core/Services/IPaymentService.cs
@@ -14,18 +14,8 @@ namespace Bit.Core.Services;
public interface IPaymentService
{
Task CancelAndRecoverChargesAsync(ISubscriber subscriber);
- Task 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 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 UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
- Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
- short additionalStorageGb, TaxInfo taxInfo);
Task AdjustSubscription(
Organization organization,
Plan updatedPlan,
@@ -56,9 +46,7 @@ public interface IPaymentService
Task SaveTaxInfoAsync(ISubscriber subscriber, TaxInfo taxInfo);
Task AddSecretsManagerToSubscription(Organization org, Plan plan, int additionalSmSeats,
int additionalServiceAccount);
- Task RisksSubscriptionFailure(Organization organization);
Task HasSecretsManagerStandalone(Organization organization);
- Task<(DateTime?, DateTime?)> GetSuspensionDateAsync(Stripe.Subscription subscription);
Task PreviewInvoiceAsync(PreviewIndividualInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
Task PreviewInvoiceAsync(PreviewOrganizationInvoiceRequestBody parameters, string gatewayCustomerId, string gatewaySubscriptionId);
diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs
index bfa94cf5ba..ca377407f4 100644
--- a/src/Core/Services/Implementations/StripePaymentService.cs
+++ b/src/Core/Services/Implementations/StripePaymentService.cs
@@ -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 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
- {
- { "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
- {
- [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 PurchaseOrganizationNoPaymentMethod(Organization org, StaticStore.Plan plan, int additionalSeats, bool premiumAccessAddon,
- int additionalSmSeats = 0, int additionalServiceAccount = 0, bool signupIsFromSecretsManagerTrial = false)
- {
-
- var stripeCustomerMetadata = new Dictionary
- {
- { "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 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 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
- {
- { "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
- {
- [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
- {
- [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 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();
- 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
- {
- [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 ToInvoiceSubscriptionItemOptions(
- List subItemOptions)
- {
- return subItemOptions.Select(si => new InvoiceSubscriptionItemOptions
- {
- Plan = si.Plan,
- Price = si.Price,
- Quantity = si.Quantity,
- Id = si.Id
- }).ToList();
- }
-
private async Task 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 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 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 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];
- }
}
diff --git a/test/Core.Test/Extensions/SubscriberExtensionsTests.cs b/test/Core.Test/Extensions/SubscriberExtensionsTests.cs
new file mode 100644
index 0000000000..e0b4cfd9f2
--- /dev/null
+++ b/test/Core.Test/Extensions/SubscriberExtensionsTests.cs
@@ -0,0 +1,23 @@
+using Bit.Core.AdminConsole.Entities.Provider;
+using Bit.Core.Billing.Extensions;
+using Xunit;
+
+namespace Bit.Core.Test.Extensions;
+
+public class SubscriberExtensionsTests
+{
+ [Theory]
+ [InlineData("Alexandria Villanueva Gonzalez Pablo", "Alexandria Villanueva Gonzalez")]
+ [InlineData("John Snow", "John Snow")]
+ public void GetFormattedInvoiceName_Returns_FirstThirtyCaractersOfName(string name, string expected)
+ {
+ // arrange
+ var provider = new Provider { Name = name };
+
+ // act
+ var actual = provider.GetFormattedInvoiceName();
+
+ // assert
+ Assert.Equal(expected, actual);
+ }
+}
diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs
deleted file mode 100644
index 11a19656e1..0000000000
--- a/test/Core.Test/Services/StripePaymentServiceTests.cs
+++ /dev/null
@@ -1,828 +0,0 @@
-using Bit.Core.AdminConsole.Entities;
-using Bit.Core.Billing.Enums;
-using Bit.Core.Billing.Services;
-using Bit.Core.Enums;
-using Bit.Core.Exceptions;
-using Bit.Core.Models.Business;
-using Bit.Core.Services;
-using Bit.Core.Settings;
-using Bit.Core.Utilities;
-using Bit.Test.Common.AutoFixture;
-using Bit.Test.Common.AutoFixture.Attributes;
-using Braintree;
-using NSubstitute;
-using Xunit;
-using Customer = Braintree.Customer;
-using PaymentMethod = Braintree.PaymentMethod;
-using PaymentMethodType = Bit.Core.Enums.PaymentMethodType;
-
-namespace Bit.Core.Test.Services;
-
-[SutProviderCustomize]
-public class StripePaymentServiceTests
-{
- [Theory]
- [BitAutoData(PaymentMethodType.BitPay)]
- [BitAutoData(PaymentMethodType.BitPay)]
- [BitAutoData(PaymentMethodType.Credit)]
- [BitAutoData(PaymentMethodType.WireTransfer)]
- [BitAutoData(PaymentMethodType.Check)]
- public async Task PurchaseOrganizationAsync_Invalid(PaymentMethodType paymentMethodType, SutProvider sutProvider)
- {
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(null, paymentMethodType, null, null, 0, 0, false, null, false, -1, -1));
-
- Assert.Equal("Payment method is not supported at this time.", exception.Message);
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo, bool provider = true)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, provider);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
-
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.Source == paymentToken &&
- c.PaymentMethod == null &&
- c.Coupon == "msp-discount-35" &&
- c.Metadata.Count == 1 &&
- c.Metadata["region"] == "US" &&
- c.InvoiceSettings.DefaultPaymentMethod == null &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 0
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_SM_Stripe_ProviderOrg_Coupon_Add(SutProvider sutProvider, Organization organization,
- string paymentToken, TaxInfo taxInfo, bool provider = true)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- organization.UseSecretsManager = true;
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
-
- });
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 1, 1,
- false, taxInfo, provider, 1, 1);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
-
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.Source == paymentToken &&
- c.PaymentMethod == null &&
- c.Coupon == "msp-discount-35" &&
- c.Metadata.Count == 1 &&
- c.Metadata["region"] == "US" &&
- c.InvoiceSettings.DefaultPaymentMethod == null &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 4
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Stripe(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- organization.UseSecretsManager = true;
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0
- , false, taxInfo, false, 8, 10);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.Source == paymentToken &&
- c.PaymentMethod == null &&
- c.Metadata.Count == 1 &&
- c.Metadata["region"] == "US" &&
- c.InvoiceSettings.DefaultPaymentMethod == null &&
- c.InvoiceSettings.CustomFields != null &&
- c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
- c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 2
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Stripe_PM(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- paymentToken = "pm_" + paymentToken;
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
-
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.Source == null &&
- c.PaymentMethod == paymentToken &&
- c.Metadata.Count == 1 &&
- c.Metadata["region"] == "US" &&
- c.InvoiceSettings.DefaultPaymentMethod == paymentToken &&
- c.InvoiceSettings.CustomFields != null &&
- c.InvoiceSettings.CustomFields[0].Name == "Organization" &&
- c.InvoiceSettings.CustomFields[0].Value == organization.SubscriberName().Substring(0, 30) &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 0
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- paymentToken = "pm_" + paymentToken;
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- Status = "incomplete",
- LatestInvoice = new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent
- {
- Status = "requires_payment_method",
- },
- },
- });
-
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo));
-
- Assert.Equal("Payment method was declined.", exception.Message);
-
- await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_SM_Stripe_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- paymentToken = "pm_" + paymentToken;
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- Status = "incomplete",
- LatestInvoice = new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent
- {
- Status = "requires_payment_method",
- },
- },
- });
-
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
- 1, 12, false, taxInfo, false, 10, 10));
-
- Assert.Equal("Payment method was declined.", exception.Message);
-
- await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- Status = "incomplete",
- LatestInvoice = new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent
- {
- Status = "requires_action",
- ClientSecret = "clientSecret",
- },
- },
- });
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo);
-
- Assert.Equal("clientSecret", result);
- Assert.False(organization.Enabled);
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_SM_Stripe_RequiresAction(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- Status = "incomplete",
- LatestInvoice = new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent
- {
- Status = "requires_action",
- ClientSecret = "clientSecret",
- },
- },
- });
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan,
- 10, 10, false, taxInfo, false, 10, 10);
-
- Assert.Equal("clientSecret", result);
- Assert.False(organization.Enabled);
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Paypal(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
-
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var customer = Substitute.For();
- customer.Id.ReturnsForAnyArgs("Braintree-Id");
- customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() });
- var customerResult = Substitute.For>();
- customerResult.IsSuccess().Returns(true);
- customerResult.Target.ReturnsForAnyArgs(customer);
-
- var braintreeGateway = sutProvider.GetDependency();
- braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
-
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
-
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.PaymentMethod == null &&
- c.Metadata.Count == 2 &&
- c.Metadata["btCustomerId"] == "Braintree-Id" &&
- c.Metadata["region"] == "US" &&
- c.InvoiceSettings.DefaultPaymentMethod == null &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 0
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_SM_Paypal(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- organization.UseSecretsManager = true;
-
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == taxInfo.BillingAddressCountry), Arg.Is(p => p == taxInfo.TaxIdNumber))
- .Returns(taxInfo.TaxIdType);
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
-
- var customer = Substitute.For();
- customer.Id.ReturnsForAnyArgs("Braintree-Id");
- customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() });
- var customerResult = Substitute.For>();
- customerResult.IsSuccess().Returns(true);
- customerResult.Target.ReturnsForAnyArgs(customer);
-
- var braintreeGateway = sutProvider.GetDependency();
- braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
-
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
-
- var additionalStorage = (short)2;
- var additionalSeats = 10;
- var additionalSmSeats = 5;
- var additionalServiceAccounts = 20;
- var result = await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
- additionalStorage, additionalSeats, false, taxInfo, false, additionalSmSeats, additionalServiceAccounts);
-
- Assert.Null(result);
- Assert.Equal(GatewayType.Stripe, organization.Gateway);
- Assert.Equal("C-1", organization.GatewayCustomerId);
- Assert.Equal("S-1", organization.GatewaySubscriptionId);
- Assert.True(organization.Enabled);
- Assert.Equal(DateTime.Today.AddDays(10), organization.ExpirationDate);
-
- await stripeAdapter.Received().CustomerCreateAsync(Arg.Is(c =>
- c.Description == organization.BusinessName &&
- c.Email == organization.BillingEmail &&
- c.PaymentMethod == null &&
- c.Metadata.Count == 2 &&
- c.Metadata["region"] == "US" &&
- c.Metadata["btCustomerId"] == "Braintree-Id" &&
- c.InvoiceSettings.DefaultPaymentMethod == null &&
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState &&
- c.TaxIdData.First().Value == taxInfo.TaxIdNumber &&
- c.TaxIdData.First().Type == taxInfo.TaxIdType
- ));
-
- await stripeAdapter.Received().SubscriptionCreateAsync(Arg.Is(s =>
- s.Customer == "C-1" &&
- s.Expand[0] == "latest_invoice.payment_intent" &&
- s.Metadata[organization.GatewayIdField()] == organization.Id.ToString() &&
- s.Items.Count == 4 &&
- s.Items.Count(i => i.Plan == plan.PasswordManager.StripeSeatPlanId && i.Quantity == additionalSeats) == 1 &&
- s.Items.Count(i => i.Plan == plan.PasswordManager.StripeStoragePlanId && i.Quantity == additionalStorage) == 1 &&
- s.Items.Count(i => i.Plan == plan.SecretsManager.StripeSeatPlanId && i.Quantity == additionalSmSeats) == 1 &&
- s.Items.Count(i => i.Plan == plan.SecretsManager.StripeServiceAccountPlanId && i.Quantity == additionalServiceAccounts) == 1
- ));
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- var customerResult = Substitute.For>();
- customerResult.IsSuccess().Returns(false);
-
- var braintreeGateway = sutProvider.GetDependency();
- braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
-
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan, 0, 0, false, taxInfo));
-
- Assert.Equal("Failed to create PayPal customer record.", exception.Message);
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_SM_Paypal_FailedCreate(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- var customerResult = Substitute.For>();
- customerResult.IsSuccess().Returns(false);
-
- var braintreeGateway = sutProvider.GetDependency();
- braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
-
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plan,
- 1, 1, false, taxInfo, false, 8, 8));
-
- Assert.Equal("Failed to create PayPal customer record.", exception.Message);
- }
-
- [Theory, BitAutoData]
- public async Task PurchaseOrganizationAsync_PayPal_Declined(SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- var plans = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- paymentToken = "pm_" + paymentToken;
-
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- Status = "incomplete",
- LatestInvoice = new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent
- {
- Status = "requires_payment_method",
- },
- },
- });
-
- var customer = Substitute.For();
- customer.Id.ReturnsForAnyArgs("Braintree-Id");
- customer.PaymentMethods.ReturnsForAnyArgs(new[] { Substitute.For() });
- var customerResult = Substitute.For>();
- customerResult.IsSuccess().Returns(true);
- customerResult.Target.ReturnsForAnyArgs(customer);
-
- var braintreeGateway = sutProvider.GetDependency();
- braintreeGateway.Customer.CreateAsync(default).ReturnsForAnyArgs(customerResult);
-
- var exception = await Assert.ThrowsAsync(
- () => sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.PayPal, paymentToken, plans, 0, 0, false, taxInfo));
-
- Assert.Equal("Payment method was declined.", exception.Message);
-
- await stripeAdapter.Received(1).CustomerDeleteAsync("C-1");
- await braintreeGateway.Customer.Received(1).DeleteAsync("Braintree-Id");
- }
-
- [Theory]
- [BitAutoData("ES", "A5372895732985327895237")]
- public async Task PurchaseOrganizationAsync_ThrowsBadRequestException_WhenTaxIdInvalid(string country, string taxId, SutProvider sutProvider, Organization organization, string paymentToken, TaxInfo taxInfo)
- {
- taxInfo.BillingAddressCountry = country;
- taxInfo.TaxIdNumber = taxId;
- taxInfo.TaxIdType = null;
-
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- organization.UseSecretsManager = true;
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerCreateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription
- {
- Id = "S-1",
- CurrentPeriodEnd = DateTime.Today.AddDays(10),
- });
- sutProvider.GetDependency()
- .BaseServiceUri.CloudRegion
- .Returns("US");
- sutProvider
- .GetDependency()
- .GetStripeTaxCode(Arg.Is(p => p == country), Arg.Is(p => p == taxId))
- .Returns((string)null);
-
- var actual = await Assert.ThrowsAsync(async () => await sutProvider.Sut.PurchaseOrganizationAsync(organization, PaymentMethodType.Card, paymentToken, plan, 0, 0, false, taxInfo, false, 8, 10));
-
- Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
-
- await stripeAdapter.Received(0).CustomerCreateAsync(Arg.Any());
- await stripeAdapter.Received(0).SubscriptionCreateAsync(Arg.Any());
- }
-
-
- [Theory, BitAutoData]
- public async Task UpgradeFreeOrganizationAsync_Success(SutProvider sutProvider,
- Organization organization, TaxInfo taxInfo)
- {
- organization.GatewaySubscriptionId = null;
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
- AmountDue = 0
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
-
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
-
- var upgrade = new OrganizationUpgrade()
- {
- AdditionalStorageGb = 0,
- AdditionalSeats = 0,
- PremiumAccessAddon = false,
- TaxInfo = taxInfo,
- AdditionalSmSeats = 0,
- AdditionalServiceAccounts = 0
- };
- var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
-
- Assert.Null(result);
- }
-
- [Theory, BitAutoData]
- public async Task UpgradeFreeOrganizationAsync_SM_Success(SutProvider sutProvider,
- Organization organization, TaxInfo taxInfo)
- {
- organization.GatewaySubscriptionId = null;
- var stripeAdapter = sutProvider.GetDependency();
- stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
- AmountDue = 0
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
-
- var upgrade = new OrganizationUpgrade()
- {
- AdditionalStorageGb = 1,
- AdditionalSeats = 10,
- PremiumAccessAddon = false,
- TaxInfo = taxInfo,
- AdditionalSmSeats = 5,
- AdditionalServiceAccounts = 50
- };
-
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- var result = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
-
- Assert.Null(result);
- }
-
- [Theory, BitAutoData]
- public async Task UpgradeFreeOrganizationAsync_WhenCustomerHasNoAddress_UpdatesCustomerAddressWithTaxInfo(
- SutProvider sutProvider,
- Organization organization,
- TaxInfo taxInfo)
- {
- organization.GatewaySubscriptionId = null;
- var stripeAdapter = sutProvider.GetDependency();
- var featureService = sutProvider.GetDependency();
- stripeAdapter.CustomerGetAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.CustomerUpdateAsync(default).ReturnsForAnyArgs(new Stripe.Customer
- {
- Id = "C-1",
- Metadata = new Dictionary
- {
- { "btCustomerId", "B-123" },
- }
- });
- stripeAdapter.InvoiceUpcomingAsync(default).ReturnsForAnyArgs(new Stripe.Invoice
- {
- PaymentIntent = new Stripe.PaymentIntent { Status = "requires_payment_method", },
- AmountDue = 0
- });
- stripeAdapter.SubscriptionCreateAsync(default).ReturnsForAnyArgs(new Stripe.Subscription { });
-
- var upgrade = new OrganizationUpgrade()
- {
- AdditionalStorageGb = 1,
- AdditionalSeats = 10,
- PremiumAccessAddon = false,
- TaxInfo = taxInfo,
- AdditionalSmSeats = 5,
- AdditionalServiceAccounts = 50
- };
-
- var plan = StaticStore.GetPlan(PlanType.EnterpriseAnnually);
- _ = await sutProvider.Sut.UpgradeFreeOrganizationAsync(organization, plan, upgrade);
-
- await stripeAdapter.Received()
- .CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(c =>
- c.Address.Country == taxInfo.BillingAddressCountry &&
- c.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
- c.Address.Line1 == taxInfo.BillingAddressLine1 &&
- c.Address.Line2 == taxInfo.BillingAddressLine2 &&
- c.Address.City == taxInfo.BillingAddressCity &&
- c.Address.State == taxInfo.BillingAddressState));
- }
-}