Quellcode durchsuchen

Merge pull request #27643 from overleaf/rh-pause-cancel

Terminate Recurly subscription when cancelling during final month of pause

GitOrigin-RevId: 39e4c9534621f57b3e2783599ebe521959d7401f
roo hutton vor 1 Jahr
Ursprung
Commit
467102fd1b

+ 24 - 10
services/web/app/src/Features/Subscription/RecurlyClient.js

@@ -331,18 +331,32 @@ async function cancelSubscriptionByUuid(subscriptionUuid) {
   try {
     return await client.cancelSubscription('uuid-' + subscriptionUuid)
   } catch (err) {
-    if (err instanceof recurly.errors.ValidationError) {
-      if (
-        err.message === 'Only active and future subscriptions can be canceled.'
-      ) {
-        logger.debug(
-          { subscriptionUuid },
-          'subscription cancellation failed, subscription not active'
-        )
-      }
-    } else {
+    if (!(err instanceof recurly.errors.ValidationError)) {
       throw err
     }
+
+    const errorMessage = err.message || ''
+
+    if (
+      errorMessage === 'Only active and future subscriptions can be canceled.'
+    ) {
+      logger.debug(
+        { subscriptionUuid },
+        'subscription cancellation failed, subscription not active'
+      )
+    } else if (
+      errorMessage.includes(
+        'Cannot cancel a paused subscription in the last cycle of the term'
+      )
+    ) {
+      logger.debug(
+        { subscriptionUuid },
+        'Terminating subscription in last cycle of paused term'
+      )
+      return await terminateSubscriptionByUuid(subscriptionUuid)
+    }
+
+    throw err
   }
 }
 

+ 12 - 19
services/web/app/src/Features/Subscription/SubscriptionHandler.js

@@ -134,26 +134,19 @@ async function cancelPendingSubscriptionChange(user) {
  * @param user
  */
 async function cancelSubscription(user) {
-  try {
-    const { hasSubscription, subscription } =
-      await LimitationsManager.promises.userHasSubscription(user)
-    if (hasSubscription && subscription != null) {
-      await Modules.promises.hooks.fire('cancelPaidSubscription', subscription)
-      const emailOpts = {
-        to: user.email,
-        first_name: user.first_name,
-      }
-      const ONE_HOUR_IN_MS = 1000 * 60 * 60
-      EmailHandler.sendDeferredEmail(
-        'canceledSubscription',
-        emailOpts,
-        ONE_HOUR_IN_MS
-      )
+  const { hasSubscription, subscription } =
+    await LimitationsManager.promises.userHasSubscription(user)
+  if (hasSubscription && subscription != null) {
+    await Modules.promises.hooks.fire('cancelPaidSubscription', subscription)
+    const emailOpts = {
+      to: user.email,
+      first_name: user.first_name,
     }
-  } catch (err) {
-    logger.warn(
-      { err, userId: user._id },
-      'there was an error checking user v2 subscription'
+    const ONE_HOUR_IN_MS = 1000 * 60 * 60
+    EmailHandler.sendDeferredEmail(
+      'canceledSubscription',
+      emailOpts,
+      ONE_HOUR_IN_MS
     )
   }
 }

+ 1 - 8
services/web/frontend/js/features/subscription/components/dashboard/states/active/cancel-subscription-button.tsx

@@ -4,7 +4,6 @@ import { useSubscriptionDashboardContext } from '../../../../context/subscriptio
 import OLButton from '@/shared/components/ol/ol-button'
 import { PaidSubscription } from '../../../../../../../../types/subscription/dashboard/subscription'
 import { useFeatureFlag } from '@/shared/context/split-test-context'
-import { useLocation } from '@/shared/hooks/use-location'
 
 export function CancelSubscriptionButton() {
   const { t } = useTranslation()
@@ -14,7 +13,6 @@ export function CancelSubscriptionButton() {
     setModalIdShown,
     setShowCancellation,
   } = useSubscriptionDashboardContext()
-  const location = useLocation()
 
   const subscription = personalSubscription as PaidSubscription
   const isInTrial =
@@ -31,18 +29,13 @@ export function CancelSubscriptionButton() {
     useFeatureFlag('pause-subscription') &&
     !hasPendingOrActivePause &&
     planIsEligibleForPause
-  const shouldContactSupport =
-    subscription.payment.state === 'paused' &&
-    subscription.payment.remainingPauseCycles === 0
 
   function handleCancelSubscriptionClick() {
     eventTracking.sendMB('subscription-page-cancel-button-click', {
       plan_code: subscription?.planCode,
       is_trial: isInTrial,
     })
-    if (shouldContactSupport) {
-      location.assign('/contact')
-    } else if (enablePause) {
+    if (enablePause) {
       setModalIdShown('pause-subscription')
     } else {
       setShowCancellation(true)

+ 0 - 29
services/web/test/frontend/features/subscription/components/dashboard/states/active/active.test.tsx

@@ -516,35 +516,6 @@ describe('<ActiveSubscription />', function () {
     })
   })
 
-  describe('contact support for paused subscription with 0 remaining cycles', function () {
-    beforeEach(function () {
-      this.locationWrapperSandbox = sinon.createSandbox()
-      this.locationWrapperStub = this.locationWrapperSandbox.stub(location)
-    })
-
-    afterEach(function () {
-      this.locationWrapperSandbox.restore()
-    })
-
-    it('redirects to contact page when cancel button clicked', function () {
-      const pausedSubscription = cloneDeep(annualActiveSubscription)
-      pausedSubscription.payment.state = 'paused'
-      pausedSubscription.payment.remainingPauseCycles = 0
-
-      renderActiveSubscription(pausedSubscription)
-
-      const button = screen.getByRole('button', {
-        name: 'Cancel your subscription',
-      })
-      fireEvent.click(button)
-
-      expect(sendMBSpy).to.be.calledOnceWith(
-        'subscription-page-cancel-button-click'
-      )
-      expect(this.locationWrapperStub.assign).to.be.calledOnceWith('/contact')
-    })
-  })
-
   describe('group plans', function () {
     it('does not show "Change plan" option for group plans', function () {
       renderActiveSubscription(groupActiveSubscription)

+ 24 - 0
services/web/test/unit/src/Subscription/RecurlyClientTests.js

@@ -560,6 +560,30 @@ describe('RecurlyClient', function () {
         'uuid-' + this.subscription.uuid
       )
     })
+
+    it('should terminate subscription when cancellation fails due to being in last cycle of paused term', async function () {
+      const validationError = new recurly.errors.ValidationError()
+      validationError.message =
+        'Cannot cancel a paused subscription in the last cycle of the term'
+
+      this.client.cancelSubscription = sinon.stub().throws(validationError)
+      this.client.terminateSubscription = sinon
+        .stub()
+        .resolves(this.recurlySubscription)
+
+      const subscription =
+        await this.RecurlyClient.promises.cancelSubscriptionByUuid(
+          this.subscription.uuid
+        )
+
+      expect(this.client.cancelSubscription).to.be.calledWith(
+        'uuid-' + this.subscription.uuid
+      )
+      expect(this.client.terminateSubscription).to.be.calledWith(
+        'uuid-' + this.subscription.uuid
+      )
+      expect(subscription).to.deep.equal(this.recurlySubscription)
+    })
   })
 
   describe('pauseSubscriptionByUuid', function () {