diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 8c013a2f22..f534fd7a1e 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -35,6 +35,20 @@ public static class Constants
/// If true, the organization plan assigned to that provider is updated to a 2020 plan.
///
public static readonly DateTime ProviderCreatedPriorNov62023 = new DateTime(2023, 11, 6);
+
+ ///
+ /// 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";
}
public static class AuthConstants
@@ -117,6 +131,7 @@ public static class FeatureFlagKeys
public const string FlexibleCollectionsMigration = "flexible-collections-migration";
public const string AC1607_PresentUsersWithOffboardingSurvey = "AC-1607_present-user-offboarding-survey";
public const string PM5766AutomaticTax = "PM-5766-automatic-tax";
+ public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
public static List GetAllKeys()
{
diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs
index fcfa40d181..1f7d488179 100644
--- a/src/Core/Services/Implementations/StripePaymentService.cs
+++ b/src/Core/Services/Implementations/StripePaymentService.cs
@@ -230,7 +230,7 @@ public class StripePaymentService : IPaymentService
null;
var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship);
- await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow);
+ await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow, true);
var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId);
org.ExpirationDate = sub.CurrentPeriodEnd;
@@ -743,12 +743,14 @@ public class StripePaymentService : IPaymentService
return subItemOptions.Select(si => new Stripe.InvoiceSubscriptionItemOptions
{
Plan = si.Plan,
- Quantity = si.Quantity
+ Price = si.Price,
+ Quantity = si.Quantity,
+ Id = si.Id
}).ToList();
}
private async Task FinalizeSubscriptionChangeAsync(IStorableSubscriber storableSubscriber,
- SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
+ SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate, bool invoiceNow = false)
{
// remember, when in doubt, throw
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
@@ -762,15 +764,37 @@ public class StripePaymentService : IPaymentService
var daysUntilDue = sub.DaysUntilDue;
var chargeNow = collectionMethod == "charge_automatically";
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
+ var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
var subUpdateOptions = new Stripe.SubscriptionUpdateOptions
{
Items = updatedItemOptions,
- ProrationBehavior = "always_invoice",
+ ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
+ ? Constants.AlwaysInvoice
+ : Constants.CreateProrations,
DaysUntilDue = daysUntilDue ?? 1,
CollectionMethod = "send_invoice",
ProrationDate = prorationDate,
};
+ var immediatelyInvoice = false;
+ if (!invoiceNow && isPm5864DollarThresholdEnabled)
+ {
+ var upcomingInvoiceWithChanges = await _stripeAdapter.InvoiceUpcomingAsync(new UpcomingInvoiceOptions
+ {
+ Customer = storableSubscriber.GatewayCustomerId,
+ Subscription = storableSubscriber.GatewaySubscriptionId,
+ SubscriptionItems = ToInvoiceSubscriptionItemOptions(updatedItemOptions),
+ SubscriptionProrationBehavior = Constants.CreateProrations,
+ SubscriptionProrationDate = prorationDate,
+ SubscriptionBillingCycleAnchor = SubscriptionBillingCycleAnchor.Now
+ });
+
+ immediatelyInvoice = upcomingInvoiceWithChanges.AmountRemaining >= 50000;
+
+ subUpdateOptions.BillingCycleAnchor = immediatelyInvoice
+ ? SubscriptionBillingCycleAnchor.Now
+ : SubscriptionBillingCycleAnchor.Unchanged;
+ }
var pm5766AutomaticTaxIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5766AutomaticTax);
if (pm5766AutomaticTaxIsEnabled)
@@ -820,19 +844,21 @@ public class StripePaymentService : IPaymentService
{
try
{
- if (chargeNow)
+ if (!isPm5864DollarThresholdEnabled || immediatelyInvoice || invoiceNow)
{
- paymentIntentClientSecret = await PayInvoiceAfterSubscriptionChangeAsync(
- storableSubscriber, invoice);
- }
- else
- {
- invoice = await _stripeAdapter.InvoiceFinalizeInvoiceAsync(subResponse.LatestInvoiceId, new Stripe.InvoiceFinalizeOptions
+ if (chargeNow)
{
- AutoAdvance = false,
- });
- await _stripeAdapter.InvoiceSendInvoiceAsync(invoice.Id, new Stripe.InvoiceSendOptions());
- paymentIntentClientSecret = null;
+ 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
@@ -896,7 +922,7 @@ public class StripePaymentService : IPaymentService
PurchasedAdditionalSecretsManagerServiceAccounts = newlyPurchasedAdditionalSecretsManagerServiceAccounts,
PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
}),
- prorationDate);
+ prorationDate, true);
public Task AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
{
@@ -1703,7 +1729,9 @@ public class StripePaymentService : IPaymentService
public async Task AddSecretsManagerToSubscription(Organization org, StaticStore.Plan plan, int additionalSmSeats,
int additionalServiceAccount, DateTime? prorationDate = null)
{
- return await FinalizeSubscriptionChangeAsync(org, new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate);
+ return await FinalizeSubscriptionChangeAsync(org,
+ new SecretsManagerSubscribeUpdate(org, plan, additionalSmSeats, additionalServiceAccount), prorationDate,
+ true);
}
public async Task RisksSubscriptionFailure(Organization organization)