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