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