Просмотр исходного кода

Merge pull request #27141 from overleaf/rh-stripe-schedule-pause

Support subscription pausing in Stripe

GitOrigin-RevId: 5550b2af2db99fd456d591c9bb4ba64d34dc7615
roo hutton 1 год назад
Родитель
Сommit
86d9f55f38

+ 3 - 2
services/web/app/src/Features/Subscription/SubscriptionHandler.js

@@ -319,8 +319,9 @@ async function pauseSubscription(user, pauseCycles) {
     throw new Error('Cannot pause a subscription with addons')
   }
 
-  await RecurlyClient.promises.pauseSubscriptionByUuid(
-    subscription.recurlySubscription_id,
+  await Modules.promises.hooks.fire(
+    'pausePaidSubscription',
+    subscription,
     pauseCycles
   )
 }

+ 28 - 7
services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js

@@ -17,6 +17,7 @@ const { V1ConnectionError } = require('../Errors/Errors')
 const FeaturesHelper = require('./FeaturesHelper')
 const { formatCurrency } = require('../../util/currency')
 const Modules = require('../../infrastructure/Modules')
+const SplitTestHandler = require('../SplitTests/SplitTestHandler')
 
 /**
  * @import { Subscription } from "../../../../types/project/dashboard/subscription"
@@ -254,6 +255,32 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
     const isInTrial =
       paymentRecord.subscription.trialPeriodEnd &&
       paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now()
+
+    let isEligibleForPause = false
+    const commonPauseConditions =
+      !personalSubscription.pendingPlan &&
+      !personalSubscription.groupPlan &&
+      !isInTrial &&
+      !paymentRecord.subscription.planCode.includes('ann') &&
+      !paymentRecord.subscription.addOns?.length
+
+    if (
+      paymentRecord.subscription.service === 'recurly' &&
+      commonPauseConditions
+    ) {
+      isEligibleForPause = true
+    } else if (
+      paymentRecord.subscription.service.includes('stripe') &&
+      commonPauseConditions
+    ) {
+      const stripePauseAssignment =
+        await SplitTestHandler.promises.getAssignmentForUser(
+          user._id,
+          'stripe-pause'
+        )
+      isEligibleForPause = stripePauseAssignment.variant === 'enabled'
+    }
+
     personalSubscription.payment = {
       taxRate,
       billingDetailsLink:
@@ -281,13 +308,7 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
       hasPastDueInvoice: paymentRecord.account.hasPastDueInvoice,
       pausedAt: paymentRecord.subscription.pausePeriodStart,
       remainingPauseCycles: paymentRecord.subscription.remainingPauseCycles,
-      isEligibleForPause:
-        paymentRecord.subscription.service === 'recurly' &&
-        !personalSubscription.pendingPlan &&
-        !personalSubscription.groupPlan &&
-        !isInTrial &&
-        !paymentRecord.subscription.planCode.includes('ann') &&
-        !paymentRecord.subscription.addOns?.length > 0,
+      isEligibleForPause,
       isEligibleForGroupPlan: await isEligibleForGroupPlan(
         paymentRecord.subscription.service,
         isInTrial

+ 10 - 3
services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js

@@ -497,11 +497,18 @@ describe('SubscriptionHandler', function () {
           },
         })
       })
-      it('should make a pause call to recurly', async function () {
+      it('should call pause hook', async function () {
         await this.SubscriptionHandler.promises.pauseSubscription(this.user, 3)
 
-        this.RecurlyClient.promises.pauseSubscriptionByUuid.called.should.equal(
-          true
+        expect(this.Modules.promises.hooks.fire).to.have.been.calledWith(
+          'pausePaidSubscription',
+          {
+            recurlySubscription_id: this.activeRecurlySubscription.uuid,
+            recurlyStatus: { state: 'non-trial' },
+            planCode: 'collaborator',
+            addOns: [],
+          },
+          3
         )
       })
     })

+ 51 - 1
services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js

@@ -704,8 +704,58 @@ describe('SubscriptionViewModelBuilder', function () {
       })
 
       describe('isEligibleForPause', function () {
-        it('is false for Stripe subscriptions', async function () {
+        beforeEach(function () {
+          this.paymentRecord.service = 'recurly'
+          this.paymentRecord.addOns = []
+          this.paymentRecord.planCode = 'plan-code'
+          this.paymentRecord.trialPeriodEnd = null
+          this.individualSubscription.pendingPlan = undefined
+          this.individualSubscription.groupPlan = undefined
+        })
+
+        it('is false for Stripe subscriptions when feature flag is disabled', async function () {
+          this.paymentRecord.service = 'stripe-us'
+          this.SplitTestHandler.promises.getAssignmentForUser
+            .withArgs(this.user._id, 'stripe-pause')
+            .resolves({ variant: 'default' })
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
+        })
+
+        it('is true for Stripe subscriptions when feature flag is enabled', async function () {
+          this.paymentRecord.service = 'stripe-us'
+          this.SplitTestHandler.promises.getAssignmentForUser
+            .withArgs(this.user._id, 'stripe-pause')
+            .resolves({ variant: 'enabled' })
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isTrue(result.personalSubscription.payment.isEligibleForPause)
+        })
+
+        it('is false for Stripe subscriptions with pending plan even when feature flag is enabled', async function () {
+          this.paymentRecord.service = 'stripe-us'
+          this.individualSubscription.pendingPlan = {} // anything
+          this.SplitTestHandler.promises.getAssignmentForUser
+            .withArgs(this.user._id, 'stripe-pause')
+            .resolves({ variant: 'enabled' })
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
+        })
+
+        it('is false for Stripe subscriptions with annual plan even when feature flag is enabled', async function () {
           this.paymentRecord.service = 'stripe-us'
+          this.paymentRecord.planCode = 'collaborator-annual'
+          this.SplitTestHandler.promises.getAssignmentForUser
+            .withArgs(this.user._id, 'stripe-pause')
+            .resolves({ variant: 'enabled' })
           const result =
             await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
               this.user