From 03b91366231348772da79fd2d728343537493468 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 20 Nov 2023 16:30:48 -0500 Subject: [PATCH] Revert "[PM-3892] Implement dollar threshold for all subscriptions (#3283)" (#3455) This reverts commit d9faa9a6dfd7d68b6f19c1d1ba9bb40b6d54496f. --- src/Core/Constants.cs | 14 - .../Models/Business/InvoicePreviewResult.cs | 7 - .../Models/Business/PendingInoviceItems.cs | 9 - .../Business/SecretsManagerSubscribeUpdate.cs | 8 +- src/Core/Services/IStripeAdapter.cs | 4 - .../Services/Implementations/StripeAdapter.cs | 18 - .../Implementations/StripePaymentService.cs | 392 +++--------------- .../Services/StripePaymentServiceTests.cs | 296 ------------- 8 files changed, 56 insertions(+), 692 deletions(-) delete mode 100644 src/Core/Models/Business/InvoicePreviewResult.cs delete mode 100644 src/Core/Models/Business/PendingInoviceItems.cs diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index c72680cd2..258c67b7f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -25,20 +25,6 @@ public static class Constants public const string CipherKeyEncryptionMinimumVersion = "2023.9.2"; - /// - /// When you set the ProrationBehavior to create_prorations, - /// Stripe will automatically create prorations for any changes made to the subscription, - /// such as changing the plan, adding or removing quantities, or applying discounts. - /// - public const string CreateProrations = "create_prorations"; - - /// - /// When you set the ProrationBehavior to always_invoice, - /// Stripe will always generate an invoice when a subscription update occurs, - /// regardless of whether there is a proration or not. - /// - public const string AlwaysInvoice = "always_invoice"; - /// /// Used by IdentityServer to identify our own provider. /// diff --git a/src/Core/Models/Business/InvoicePreviewResult.cs b/src/Core/Models/Business/InvoicePreviewResult.cs deleted file mode 100644 index d9e211cfb..000000000 --- a/src/Core/Models/Business/InvoicePreviewResult.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace Bit.Core.Models.Business; - -public class InvoicePreviewResult -{ - public bool IsInvoicedNow { get; set; } - public string PaymentIntentClientSecret { get; set; } -} diff --git a/src/Core/Models/Business/PendingInoviceItems.cs b/src/Core/Models/Business/PendingInoviceItems.cs deleted file mode 100644 index 1aee15a3a..000000000 --- a/src/Core/Models/Business/PendingInoviceItems.cs +++ /dev/null @@ -1,9 +0,0 @@ -using Stripe; - -namespace Bit.Core.Models.Business; - -public class PendingInoviceItems -{ - public IEnumerable PendingInvoiceItems { get; set; } - public IDictionary PendingInvoiceItemsDict { get; set; } -} diff --git a/src/Core/Models/Business/SecretsManagerSubscribeUpdate.cs b/src/Core/Models/Business/SecretsManagerSubscribeUpdate.cs index 54bc8cb95..8f3fb8934 100644 --- a/src/Core/Models/Business/SecretsManagerSubscribeUpdate.cs +++ b/src/Core/Models/Business/SecretsManagerSubscribeUpdate.cs @@ -44,7 +44,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate { updatedItems.Add(new SubscriptionItemOptions { - Plan = _plan.SecretsManager.StripeSeatPlanId, + Price = _plan.SecretsManager.StripeSeatPlanId, Quantity = _additionalSeats }); } @@ -53,7 +53,7 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate { updatedItems.Add(new SubscriptionItemOptions { - Plan = _plan.SecretsManager.StripeServiceAccountPlanId, + Price = _plan.SecretsManager.StripeServiceAccountPlanId, Quantity = _additionalServiceAccounts }); } @@ -63,14 +63,14 @@ public class SecretsManagerSubscribeUpdate : SubscriptionUpdate { updatedItems.Add(new SubscriptionItemOptions { - Plan = _plan.SecretsManager.StripeSeatPlanId, + Price = _plan.SecretsManager.StripeSeatPlanId, Quantity = _previousSeats, Deleted = _previousSeats == 0 ? true : (bool?)null, }); updatedItems.Add(new SubscriptionItemOptions { - Plan = _plan.SecretsManager.StripeServiceAccountPlanId, + Price = _plan.SecretsManager.StripeServiceAccountPlanId, Quantity = _previousServiceAccounts, Deleted = _previousServiceAccounts == 0 ? true : (bool?)null, }); diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index f79cc1200..60d14ffad 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -1,5 +1,4 @@ using Bit.Core.Models.BitStripe; -using Stripe; namespace Bit.Core.Services; @@ -15,11 +14,8 @@ public interface IStripeAdapter Task SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null); Task SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null); Task InvoiceUpcomingAsync(Stripe.UpcomingInvoiceOptions options); - Task InvoiceCreateAsync(Stripe.InvoiceCreateOptions options); - Task InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options); Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options); Task> InvoiceListAsync(StripeInvoiceListOptions options); - IEnumerable InvoiceItemListAsync(InvoiceItemListOptions options); Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options); Task InvoiceFinalizeInvoiceAsync(string id, Stripe.InvoiceFinalizeOptions options); Task InvoiceSendInvoiceAsync(string id, Stripe.InvoiceSendOptions options); diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index 28dd35034..747510d05 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -1,5 +1,4 @@ using Bit.Core.Models.BitStripe; -using Stripe; namespace Bit.Core.Services; @@ -17,7 +16,6 @@ public class StripeAdapter : IStripeAdapter private readonly Stripe.BankAccountService _bankAccountService; private readonly Stripe.PriceService _priceService; private readonly Stripe.TestHelpers.TestClockService _testClockService; - private readonly Stripe.InvoiceItemService _invoiceItemService; public StripeAdapter() { @@ -33,7 +31,6 @@ public class StripeAdapter : IStripeAdapter _bankAccountService = new Stripe.BankAccountService(); _priceService = new Stripe.PriceService(); _testClockService = new Stripe.TestHelpers.TestClockService(); - _invoiceItemService = new Stripe.InvoiceItemService(); } public Task CustomerCreateAsync(Stripe.CustomerCreateOptions options) @@ -82,16 +79,6 @@ public class StripeAdapter : IStripeAdapter return _invoiceService.UpcomingAsync(options); } - public Task InvoiceCreateAsync(Stripe.InvoiceCreateOptions options) - { - return _invoiceService.CreateAsync(options); - } - - public Task InvoiceItemCreateAsync(Stripe.InvoiceItemCreateOptions options) - { - return _invoiceItemService.CreateAsync(options); - } - public Task InvoiceGetAsync(string id, Stripe.InvoiceGetOptions options) { return _invoiceService.GetAsync(id, options); @@ -116,11 +103,6 @@ public class StripeAdapter : IStripeAdapter return invoices; } - public IEnumerable InvoiceItemListAsync(InvoiceItemListOptions options) - { - return _invoiceItemService.ListAutoPaging(options); - } - public Task InvoiceUpdateAsync(string id, Stripe.InvoiceUpdateOptions options) { return _invoiceService.UpdateAsync(id, options); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 67de73a18..320145ecd 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -7,7 +7,6 @@ using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Settings; using Microsoft.Extensions.Logging; -using Stripe; using StaticStore = Bit.Core.Models.StaticStore; using TaxRate = Bit.Core.Entities.TaxRate; @@ -751,14 +750,16 @@ public class StripePaymentService : IPaymentService prorationDate ??= DateTime.UtcNow; var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; + var chargeNow = collectionMethod == "charge_automatically"; var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); var subUpdateOptions = new Stripe.SubscriptionUpdateOptions { Items = updatedItemOptions, - ProrationBehavior = Constants.CreateProrations, + ProrationBehavior = "always_invoice", DaysUntilDue = daysUntilDue ?? 1, - CollectionMethod = "send_invoice" + CollectionMethod = "send_invoice", + ProrationDate = prorationDate, }; if (!subscriptionUpdate.UpdateNeeded(sub)) @@ -792,50 +793,66 @@ public class StripePaymentService : IPaymentService string paymentIntentClientSecret = null; try { - var subItemOptions = updatedItemOptions.Select(itemOption => - new Stripe.InvoiceSubscriptionItemOptions - { - Id = itemOption.Id, - Plan = itemOption.Plan, - Quantity = itemOption.Quantity, - }).ToList(); - - var reviewInvoiceResponse = await PreviewUpcomingInvoiceAndPayAsync(storableSubscriber, subItemOptions); - paymentIntentClientSecret = reviewInvoiceResponse.PaymentIntentClientSecret; - var subResponse = await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, subUpdateOptions); - var invoice = - await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions()); + + var invoice = await _stripeAdapter.InvoiceGetAsync(subResponse?.LatestInvoiceId, new Stripe.InvoiceGetOptions()); if (invoice == null) { throw new BadRequestException("Unable to locate draft invoice for subscription update."); } - } - catch (Exception e) - { - // Need to revert the subscription - await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions + + if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0)) { - Items = subscriptionUpdate.RevertItemsOptions(sub), - // This proration behavior prevents a false "credit" from - // being applied forward to the next month's invoice - ProrationBehavior = "none", - CollectionMethod = collectionMethod, - DaysUntilDue = daysUntilDue, - }); - throw; + try + { + if (chargeNow) + { + paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync( + storableSubscriber, invoice); + } + else + { + invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions + { + AutoAdvance = false, + }); + await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions()); + paymentIntentClientSecret = null; + } + } + catch + { + // Need to revert the subscription + await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions + { + Items = subscriptionUpdate.RevertItemsOptions(sub), + // This proration behavior prevents a false "credit" from + // being applied forward to the next month's invoice + ProrationBehavior = "none", + CollectionMethod = collectionMethod, + DaysUntilDue = daysUntilDue, + }); + throw; + } + } + else if (!invoice.Paid) + { + // Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h + invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId); + paymentIntentClientSecret = null; + } + } finally { // Change back the subscription collection method and/or days until due if (collectionMethod != "send_invoice" || daysUntilDue == null) { - await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, - new Stripe.SubscriptionUpdateOptions - { - CollectionMethod = collectionMethod, - DaysUntilDue = daysUntilDue, - }); + await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions + { + CollectionMethod = collectionMethod, + DaysUntilDue = daysUntilDue, + }); } } @@ -918,7 +935,6 @@ public class StripePaymentService : IPaymentService await _stripeAdapter.CustomerDeleteAsync(subscriber.GatewayCustomerId); } - //This method is no-longer is use because we return the dollar threshold feature on invoice will be generated. but we dont want to lose this implementation. public async Task PayInvoiceAfterSubscriptionChangeAsync(ISubscriber subscriber, Stripe.Invoice invoice) { var customerOptions = new Stripe.CustomerGetOptions(); @@ -1088,310 +1104,6 @@ public class StripePaymentService : IPaymentService return paymentIntentClientSecret; } - internal async Task PreviewUpcomingInvoiceAndPayAsync(ISubscriber subscriber, - List subItemOptions, int prorateThreshold = 50000) - { - var customer = await CheckInAppPurchaseMethod(subscriber); - - string paymentIntentClientSecret = null; - - var pendingInvoiceItems = GetPendingInvoiceItems(subscriber); - - var upcomingPreview = await GetUpcomingInvoiceAsync(subscriber, subItemOptions); - - var itemsForInvoice = GetItemsForInvoice(subItemOptions, upcomingPreview, pendingInvoiceItems); - var invoiceAmount = itemsForInvoice?.Sum(i => i.Amount) ?? 0; - var invoiceNow = invoiceAmount >= prorateThreshold; - if (invoiceNow) - { - await ProcessImmediateInvoiceAsync(subscriber, upcomingPreview, invoiceAmount, customer, itemsForInvoice, pendingInvoiceItems, paymentIntentClientSecret); - } - - return new InvoicePreviewResult { IsInvoicedNow = invoiceNow, PaymentIntentClientSecret = paymentIntentClientSecret }; - } - - private async Task ProcessImmediateInvoiceAsync(ISubscriber subscriber, Invoice upcomingPreview, long invoiceAmount, - Customer customer, IEnumerable itemsForInvoice, PendingInoviceItems pendingInvoiceItems, - string paymentIntentClientSecret) - { - // Owes more than prorateThreshold on the next invoice. - // Invoice them and pay now instead of waiting until the next billing cycle. - - string cardPaymentMethodId = null; - var invoiceAmountDue = upcomingPreview.StartingBalance + invoiceAmount; - cardPaymentMethodId = GetCardPaymentMethodId(invoiceAmountDue, customer, cardPaymentMethodId); - - Stripe.Invoice invoice = null; - var createdInvoiceItems = new List(); - Braintree.Transaction braintreeTransaction = null; - - try - { - await CreateInvoiceItemsAsync(subscriber, itemsForInvoice, pendingInvoiceItems, createdInvoiceItems); - - invoice = await CreateInvoiceAsync(subscriber, cardPaymentMethodId); - - var invoicePayOptions = new Stripe.InvoicePayOptions(); - await CreateBrainTreeTransactionRequestAsync(subscriber, invoice, customer, invoicePayOptions, - cardPaymentMethodId, braintreeTransaction); - - await InvoicePayAsync(invoicePayOptions, invoice, paymentIntentClientSecret); - } - catch (Exception e) - { - if (braintreeTransaction != null) - { - await _btGateway.Transaction.RefundAsync(braintreeTransaction.Id); - } - - if (invoice != null) - { - if (invoice.Status == "paid") - { - // It's apparently paid, so we return without throwing an exception - return new InvoicePreviewResult - { - IsInvoicedNow = false, - PaymentIntentClientSecret = paymentIntentClientSecret - }; - } - - await RestoreInvoiceItemsAsync(invoice, customer, pendingInvoiceItems.PendingInvoiceItems); - } - else - { - foreach (var ii in createdInvoiceItems) - { - await _stripeAdapter.InvoiceDeleteAsync(ii.Id); - } - } - - if (e is Stripe.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; - } - - return new InvoicePreviewResult - { - IsInvoicedNow = false, - PaymentIntentClientSecret = paymentIntentClientSecret - }; - } - - private static IEnumerable GetItemsForInvoice(List subItemOptions, Invoice upcomingPreview, - PendingInoviceItems pendingInvoiceItems) - { - var itemsForInvoice = upcomingPreview.Lines?.Data? - .Where(i => pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(i.Id) || - (i.Plan.Id == subItemOptions[0]?.Plan && i.Proration)); - return itemsForInvoice; - } - - private PendingInoviceItems GetPendingInvoiceItems(ISubscriber subscriber) - { - var pendingInvoiceItems = new PendingInoviceItems(); - var invoiceItems = _stripeAdapter.InvoiceItemListAsync(new Stripe.InvoiceItemListOptions - { - Customer = subscriber.GatewayCustomerId - }).ToList().Where(i => i.InvoiceId == null); - pendingInvoiceItems.PendingInvoiceItemsDict = invoiceItems.ToDictionary(pii => pii.Id); - return pendingInvoiceItems; - } - - private async Task CheckInAppPurchaseMethod(ISubscriber subscriber) - { - var customerOptions = GetCustomerPaymentOptions(); - var customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, customerOptions); - var usingInAppPaymentMethod = customer.Metadata.ContainsKey("appleReceipt"); - if (usingInAppPaymentMethod) - { - throw new BadRequestException("Cannot perform this action with in-app purchase payment method. " + - "Contact support."); - } - - return customer; - } - - private string GetCardPaymentMethodId(long invoiceAmountDue, Customer customer, string cardPaymentMethodId) - { - try - { - if (invoiceAmountDue <= 0 || customer.Metadata.ContainsKey("btCustomerId")) return cardPaymentMethodId; - var hasDefaultCardPaymentMethod = customer.InvoiceSettings?.DefaultPaymentMethod?.Type == "card"; - var hasDefaultValidSource = customer.DefaultSource != null && - (customer.DefaultSource is Stripe.Card || - customer.DefaultSource is Stripe.BankAccount); - if (hasDefaultCardPaymentMethod || hasDefaultValidSource) return cardPaymentMethodId; - cardPaymentMethodId = GetLatestCardPaymentMethod(customer.Id)?.Id; - if (cardPaymentMethodId == null) - { - throw new BadRequestException("No payment method is available."); - } - } - catch (Exception e) - { - throw new BadRequestException("No payment method is available."); - } - - - return cardPaymentMethodId; - } - - private async Task GetUpcomingInvoiceAsync(ISubscriber subscriber, List subItemOptions) - { - var upcomingPreview = await _stripeAdapter.InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions - { - Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, - SubscriptionItems = subItemOptions - }); - return upcomingPreview; - } - - private async Task RestoreInvoiceItemsAsync(Invoice invoice, Customer customer, IEnumerable pendingInvoiceItems) - { - invoice = await _stripeAdapter.InvoiceVoidInvoiceAsync(invoice.Id, new Stripe.InvoiceVoidOptions()); - if (invoice.StartingBalance != 0) - { - await _stripeAdapter.CustomerUpdateAsync(customer.Id, - new Stripe.CustomerUpdateOptions { Balance = customer.Balance }); - } - - // Restore invoice items that were brought in - foreach (var item in pendingInvoiceItems) - { - var i = new Stripe.InvoiceItemCreateOptions - { - Currency = item.Currency, - Description = item.Description, - Customer = item.CustomerId, - Subscription = item.SubscriptionId, - Discountable = item.Discountable, - Metadata = item.Metadata, - Quantity = item.Proration ? 1 : item.Quantity, - UnitAmount = item.UnitAmount - }; - await _stripeAdapter.InvoiceItemCreateAsync(i); - } - } - - private async Task InvoicePayAsync(InvoicePayOptions invoicePayOptions, Invoice invoice, string paymentIntentClientSecret) - { - try - { - await _stripeAdapter.InvoicePayAsync(invoice.Id, invoicePayOptions); - } - catch (Stripe.StripeException e) - { - if (e.HttpStatusCode == System.Net.HttpStatusCode.PaymentRequired && - e.StripeError?.Code == "invoice_payment_intent_requires_action") - { - // SCA required, get intent client secret - var invoiceGetOptions = new Stripe.InvoiceGetOptions(); - invoiceGetOptions.AddExpand("payment_intent"); - invoice = await _stripeAdapter.InvoiceGetAsync(invoice.Id, invoiceGetOptions); - paymentIntentClientSecret = invoice?.PaymentIntent?.ClientSecret; - } - else - { - throw new GatewayException("Unable to pay invoice."); - } - } - } - - private async Task CreateBrainTreeTransactionRequestAsync(ISubscriber subscriber, Invoice invoice, Customer customer, - InvoicePayOptions invoicePayOptions, string cardPaymentMethodId, Braintree.Transaction braintreeTransaction) - { - if (invoice.AmountDue > 0) - { - if (customer?.Metadata?.ContainsKey("btCustomerId") ?? false) - { - invoicePayOptions.PaidOutOfBand = true; - var btInvoiceAmount = (invoice.AmountDue / 100M); - var transactionResult = await _btGateway.Transaction.SaleAsync( - new Braintree.TransactionRequest - { - Amount = btInvoiceAmount, - CustomerId = customer.Metadata["btCustomerId"], - Options = new Braintree.TransactionOptionsRequest - { - SubmitForSettlement = true, - PayPal = new Braintree.TransactionOptionsPayPalRequest - { - CustomField = $"{subscriber.BraintreeIdField()}:{subscriber.Id}" - } - }, - CustomFields = new Dictionary - { - [subscriber.BraintreeIdField()] = subscriber.Id.ToString() - } - }); - - if (!transactionResult.IsSuccess()) - { - throw new GatewayException("Failed to charge PayPal customer."); - } - - braintreeTransaction = transactionResult.Target; - await _stripeAdapter.InvoiceUpdateAsync(invoice.Id, new Stripe.InvoiceUpdateOptions - { - Metadata = new Dictionary - { - ["btTransactionId"] = braintreeTransaction.Id, - ["btPayPalTransactionId"] = - braintreeTransaction.PayPalDetails.AuthorizationId - } - }); - } - else - { - invoicePayOptions.OffSession = true; - invoicePayOptions.PaymentMethod = cardPaymentMethodId; - } - } - } - - private async Task CreateInvoiceAsync(ISubscriber subscriber, string cardPaymentMethodId) - { - Invoice invoice; - invoice = await _stripeAdapter.InvoiceCreateAsync(new Stripe.InvoiceCreateOptions - { - CollectionMethod = "send_invoice", - DaysUntilDue = 1, - Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, - DefaultPaymentMethod = cardPaymentMethodId - }); - return invoice; - } - - private async Task CreateInvoiceItemsAsync(ISubscriber subscriber, IEnumerable itemsForInvoice, - PendingInoviceItems pendingInvoiceItems, List createdInvoiceItems) - { - foreach (var invoiceLineItem in itemsForInvoice) - { - if (pendingInvoiceItems.PendingInvoiceItemsDict.ContainsKey(invoiceLineItem.Id)) - { - continue; - } - - var invoiceItem = await _stripeAdapter.InvoiceItemCreateAsync(new Stripe.InvoiceItemCreateOptions - { - Currency = invoiceLineItem.Currency, - Description = invoiceLineItem.Description, - Customer = subscriber.GatewayCustomerId, - Subscription = invoiceLineItem.Subscription, - Discountable = invoiceLineItem.Discountable, - Amount = invoiceLineItem.Amount - }); - createdInvoiceItems.Add(invoiceItem); - } - } - public async Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, bool skipInAppPurchaseCheck = false) { diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 9ef4b0233..2133a14a9 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -739,300 +739,4 @@ public class StripePaymentServiceTests Assert.Null(result); } - - [Theory, BitAutoData] - public async Task PreviewUpcomingInvoiceAndPayAsync_WithInAppPaymentMethod_ThrowsBadRequestException(SutProvider sutProvider, - Organization subscriber, List subItemOptions) - { - var stripeAdapter = sutProvider.GetDependency(); - stripeAdapter.CustomerGetAsync(Arg.Any(), Arg.Any()) - .Returns(new Stripe.Customer { Metadata = new Dictionary { { "appleReceipt", "dummyData" } } }); - - var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions)); - Assert.Equal("Cannot perform this action with in-app purchase payment method. Contact support.", ex.Message); - } - - [Theory, BitAutoData] - public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceBelowThreshold_DoesNotInvoiceNow(SutProvider sutProvider, - Organization subscriber, List subItemOptions) - { - var prorateThreshold = 50000; - var invoiceAmountBelowThreshold = prorateThreshold - 100; - var customer = MockStripeCustomer(subscriber); - sutProvider.GetDependency().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer); - var invoiceItem = MockInoviceItemList(subscriber, "planId", invoiceAmountBelowThreshold, customer); - sutProvider.GetDependency().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions - { - Customer = subscriber.GatewayCustomerId - }).ReturnsForAnyArgs(invoiceItem); - - var invoiceLineItem = CreateInvoiceLineTime(subscriber, "planId", invoiceAmountBelowThreshold); - sutProvider.GetDependency().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions - { - Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, - SubscriptionItems = subItemOptions - }).ReturnsForAnyArgs(invoiceLineItem); - - sutProvider.GetDependency().InvoiceCreateAsync(Arg.Is(options => - options.CollectionMethod == "send_invoice" && - options.DaysUntilDue == 1 && - options.Customer == subscriber.GatewayCustomerId && - options.Subscription == subscriber.GatewaySubscriptionId && - options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id - )).ReturnsForAnyArgs(new Stripe.Invoice - { - Id = "mockInvoiceId", - CollectionMethod = "send_invoice", - DueDate = DateTime.Now.AddDays(1), - Customer = customer, - Subscription = new Stripe.Subscription - { - Id = "mockSubscriptionId", - Customer = customer, - Status = "active", - CurrentPeriodStart = DateTime.UtcNow, - CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1), - CollectionMethod = "charge_automatically", - }, - DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod, - AmountDue = invoiceAmountBelowThreshold, - Currency = "usd", - Status = "draft", - }); - - var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List(), prorateThreshold); - - Assert.False(result.IsInvoicedNow); - Assert.Null(result.PaymentIntentClientSecret); - } - - [Theory, BitAutoData] - public async void PreviewUpcomingInvoiceAndPayAsync_NoPaymentMethod_ThrowsBadRequestException(SutProvider sutProvider, - Organization subscriber, List subItemOptions, string planId) - { - var prorateThreshold = 120000; - var invoiceAmountBelowThreshold = prorateThreshold; - var customer = new Stripe.Customer - { - Metadata = new Dictionary(), - Id = subscriber.GatewayCustomerId, - DefaultSource = null, - InvoiceSettings = new Stripe.CustomerInvoiceSettings - { - DefaultPaymentMethod = null - } - }; - sutProvider.GetDependency().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer); - var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer); - sutProvider.GetDependency().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions - { - Customer = subscriber.GatewayCustomerId - }).ReturnsForAnyArgs(invoiceItem); - - var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold); - sutProvider.GetDependency().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions - { - Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, - SubscriptionItems = subItemOptions - }).ReturnsForAnyArgs(invoiceLineItem); - - var ex = await Assert.ThrowsAsync(() => sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, subItemOptions)); - Assert.Equal("No payment method is available.", ex.Message); - } - - [Theory, BitAutoData] - public async void PreviewUpcomingInvoiceAndPayAsync_UpcomingInvoiceAboveThreshold_DoesInvoiceNow(SutProvider sutProvider, - Organization subscriber, List subItemOptions, string planId) - { - var prorateThreshold = 50000; - var invoiceAmountBelowThreshold = 100000; - var customer = MockStripeCustomer(subscriber); - sutProvider.GetDependency().CustomerGetAsync(default, default).ReturnsForAnyArgs(customer); - var invoiceItem = MockInoviceItemList(subscriber, planId, invoiceAmountBelowThreshold, customer); - sutProvider.GetDependency().InvoiceItemListAsync(new Stripe.InvoiceItemListOptions - { - Customer = subscriber.GatewayCustomerId - }).ReturnsForAnyArgs(invoiceItem); - - var invoiceLineItem = CreateInvoiceLineTime(subscriber, planId, invoiceAmountBelowThreshold); - sutProvider.GetDependency().InvoiceUpcomingAsync(new Stripe.UpcomingInvoiceOptions - { - Customer = subscriber.GatewayCustomerId, - Subscription = subscriber.GatewaySubscriptionId, - SubscriptionItems = subItemOptions - }).ReturnsForAnyArgs(invoiceLineItem); - - var invoice = MockInVoice(customer, invoiceAmountBelowThreshold); - sutProvider.GetDependency().InvoiceCreateAsync(Arg.Is(options => - options.CollectionMethod == "send_invoice" && - options.DaysUntilDue == 1 && - options.Customer == subscriber.GatewayCustomerId && - options.Subscription == subscriber.GatewaySubscriptionId && - options.DefaultPaymentMethod == customer.InvoiceSettings.DefaultPaymentMethod.Id - )).ReturnsForAnyArgs(invoice); - - var result = await sutProvider.Sut.PreviewUpcomingInvoiceAndPayAsync(subscriber, new List(), prorateThreshold); - - await sutProvider.GetDependency().Received(1).InvoicePayAsync(invoice.Id, - Arg.Is((options => - options.OffSession == true - ))); - - - Assert.True(result.IsInvoicedNow); - Assert.Null(result.PaymentIntentClientSecret); - } - - private static Stripe.Invoice MockInVoice(Stripe.Customer customer, int invoiceAmountBelowThreshold) => - new() - { - Id = "mockInvoiceId", - CollectionMethod = "send_invoice", - DueDate = DateTime.Now.AddDays(1), - Customer = customer, - Subscription = new Stripe.Subscription - { - Id = "mockSubscriptionId", - Customer = customer, - Status = "active", - CurrentPeriodStart = DateTime.UtcNow, - CurrentPeriodEnd = DateTime.UtcNow.AddMonths(1), - CollectionMethod = "charge_automatically", - }, - DefaultPaymentMethod = customer.InvoiceSettings.DefaultPaymentMethod, - AmountDue = invoiceAmountBelowThreshold, - Currency = "usd", - Status = "draft", - }; - - private static List MockInoviceItemList(Organization subscriber, string planId, int invoiceAmountBelowThreshold, Stripe.Customer customer) => - new() - { - new Stripe.InvoiceItem - { - Id = "ii_1234567890", - Amount = invoiceAmountBelowThreshold, - Currency = "usd", - CustomerId = subscriber.GatewayCustomerId, - Description = "Sample invoice item 1", - Date = DateTime.UtcNow, - Discountable = true, - InvoiceId = "548458365" - }, - new Stripe.InvoiceItem - { - Id = "ii_0987654321", - Amount = invoiceAmountBelowThreshold, - Currency = "usd", - CustomerId = customer.Id, - Description = "Sample invoice item 2", - Date = DateTime.UtcNow.AddDays(-5), - Discountable = false, - InvoiceId = null, - Proration = true, - Plan = new Stripe.Plan - { - Id = planId, - Amount = invoiceAmountBelowThreshold, - Currency = "usd", - Interval = "month", - IntervalCount = 1, - }, - } - }; - - private static Stripe.Customer MockStripeCustomer(Organization subscriber) - { - var customer = new Stripe.Customer - { - Metadata = new Dictionary(), - Id = subscriber.GatewayCustomerId, - DefaultSource = new Stripe.Card - { - Id = "card_12345", - Last4 = "1234", - Brand = "Visa", - ExpYear = 2025, - ExpMonth = 12 - }, - InvoiceSettings = new Stripe.CustomerInvoiceSettings - { - DefaultPaymentMethod = new Stripe.PaymentMethod - { - Id = "pm_12345", - Type = "card", - Card = new Stripe.PaymentMethodCard - { - Last4 = "1234", - Brand = "Visa", - ExpYear = 2025, - ExpMonth = 12 - } - } - } - }; - return customer; - } - - private static Stripe.Invoice CreateInvoiceLineTime(Organization subscriber, string planId, int invoiceAmountBelowThreshold) => - new() - { - AmountDue = invoiceAmountBelowThreshold, - AmountPaid = 0, - AmountRemaining = invoiceAmountBelowThreshold, - CustomerId = subscriber.GatewayCustomerId, - SubscriptionId = subscriber.GatewaySubscriptionId, - ApplicationFeeAmount = 0, - Currency = "usd", - Description = "Upcoming Invoice", - Discount = null, - DueDate = DateTime.UtcNow.AddDays(1), - EndingBalance = 0, - Number = "INV12345", - Paid = false, - PeriodStart = DateTime.UtcNow, - PeriodEnd = DateTime.UtcNow.AddMonths(1), - ReceiptNumber = null, - StartingBalance = 0, - Status = "draft", - Id = "ii_0987654321", - Total = invoiceAmountBelowThreshold, - Lines = new Stripe.StripeList - { - Data = new List - { - new Stripe.InvoiceLineItem - { - Amount = invoiceAmountBelowThreshold, - Currency = "usd", - Description = "Sample line item", - Id = "ii_0987654321", - Livemode = false, - Object = "line_item", - Discountable = false, - Period = new Stripe.InvoiceLineItemPeriod() - { - Start = DateTime.UtcNow, - End = DateTime.UtcNow.AddMonths(1) - }, - Plan = new Stripe.Plan - { - Id = planId, - Amount = invoiceAmountBelowThreshold, - Currency = "usd", - Interval = "month", - IntervalCount = 1, - }, - Proration = true, - Quantity = 1, - Subscription = subscriber.GatewaySubscriptionId, - SubscriptionItem = "si_12345", - Type = "subscription", - UnitAmountExcludingTax = invoiceAmountBelowThreshold, - } - } - } - }; }