From accff663c575cbb8ab3a43830d45c81f13674a54 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 13 Feb 2024 20:28:14 +0100 Subject: [PATCH] [PM 5864] Resolve root cause of double-charging customers with implementation of PM-3892 (#3762) * Getting dollar threshold to work * Added billing cycle anchor to invoice upcoming call * Added comments for further work * add featureflag Signed-off-by: Cy Okeke * resolve pr comments Signed-off-by: Cy Okeke * Resolve pr comment Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke Co-authored-by: Conner Turnbull --- src/Core/Constants.cs | 15 +++++ .../Implementations/StripePaymentService.cs | 62 ++++++++++++++----- 2 files changed, 60 insertions(+), 17 deletions(-) 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)