Browse Source

Merge pull request #24138 from overleaf/ii-flexible-licensing-colombian-pesos

[web] Recurly subtotal limit on flexible licensing

GitOrigin-RevId: 302fb15dcc360e3b47674e8e776ffa115af6cbe6
ilkin-overleaf 1 năm trước cách đây
mục cha
commit
96af83a4ed

+ 3 - 0
services/web/app/src/Features/Subscription/Errors.js

@@ -22,6 +22,8 @@ class PendingChangeError extends OError {}
 
 
 class InactiveError extends OError {}
 class InactiveError extends OError {}
 
 
+class SubtotalLimitExceededError extends OError {}
+
 module.exports = {
 module.exports = {
   RecurlyTransactionError,
   RecurlyTransactionError,
   DuplicateAddOnError,
   DuplicateAddOnError,
@@ -30,4 +32,5 @@ module.exports = {
   ManuallyCollectedError,
   ManuallyCollectedError,
   PendingChangeError,
   PendingChangeError,
   InactiveError,
   InactiveError,
+  SubtotalLimitExceededError,
 }
 }

+ 67 - 17
services/web/app/src/Features/Subscription/RecurlyClient.js

@@ -16,7 +16,10 @@ const {
   RecurlyPlan,
   RecurlyPlan,
   RecurlyImmediateCharge,
   RecurlyImmediateCharge,
 } = require('./RecurlyEntities')
 } = require('./RecurlyEntities')
-const { MissingBillingInfoError } = require('./Errors')
+const {
+  MissingBillingInfoError,
+  SubtotalLimitExceededError,
+} = require('./Errors')
 
 
 /**
 /**
  * @import { RecurlySubscriptionChangeRequest } from './RecurlyEntities'
  * @import { RecurlySubscriptionChangeRequest } from './RecurlyEntities'
@@ -116,14 +119,37 @@ async function getSubscriptionForUser(userId) {
  */
  */
 async function applySubscriptionChangeRequest(changeRequest) {
 async function applySubscriptionChangeRequest(changeRequest) {
   const body = subscriptionChangeRequestToApi(changeRequest)
   const body = subscriptionChangeRequestToApi(changeRequest)
-  const change = await client.createSubscriptionChange(
-    `uuid-${changeRequest.subscription.id}`,
-    body
-  )
-  logger.debug(
-    { subscriptionId: changeRequest.subscription.id, changeId: change.id },
-    'created subscription change'
-  )
+
+  try {
+    const change = await client.createSubscriptionChange(
+      `uuid-${changeRequest.subscription.id}`,
+      body
+    )
+    logger.debug(
+      { subscriptionId: changeRequest.subscription.id, changeId: change.id },
+      'created subscription change'
+    )
+  } catch (err) {
+    if (err instanceof recurly.errors.ValidationError) {
+      /**
+       * @type {{params?: { param?: string }[] | null}}
+       */
+      const validationError = err
+      if (
+        validationError.params?.some(
+          p => p.param === 'subtotal_amount_in_cents'
+        )
+      ) {
+        throw new SubtotalLimitExceededError(
+          'Subtotal amount in cents exceeded error',
+          {
+            subscriptionId: changeRequest.subscription.id,
+          }
+        )
+      }
+    }
+    throw err
+  }
 }
 }
 
 
 /**
 /**
@@ -134,14 +160,38 @@ async function applySubscriptionChangeRequest(changeRequest) {
  */
  */
 async function previewSubscriptionChange(changeRequest) {
 async function previewSubscriptionChange(changeRequest) {
   const body = subscriptionChangeRequestToApi(changeRequest)
   const body = subscriptionChangeRequestToApi(changeRequest)
-  const subscriptionChange = await client.previewSubscriptionChange(
-    `uuid-${changeRequest.subscription.id}`,
-    body
-  )
-  return subscriptionChangeFromApi(
-    changeRequest.subscription,
-    subscriptionChange
-  )
+
+  try {
+    const subscriptionChange = await client.previewSubscriptionChange(
+      `uuid-${changeRequest.subscription.id}`,
+      body
+    )
+
+    return subscriptionChangeFromApi(
+      changeRequest.subscription,
+      subscriptionChange
+    )
+  } catch (err) {
+    if (err instanceof recurly.errors.ValidationError) {
+      /**
+       * @type {{params?: { param?: string }[] | null}}
+       */
+      const validationError = err
+      if (
+        validationError.params?.some(
+          p => p.param === 'subtotal_amount_in_cents'
+        )
+      ) {
+        throw new SubtotalLimitExceededError(
+          'Subtotal amount in cents exceeded error',
+          {
+            subscriptionId: changeRequest.subscription.id,
+          }
+        )
+      }
+    }
+    throw err
+  }
 }
 }
 
 
 async function removeSubscriptionChange(subscriptionId) {
 async function removeSubscriptionChange(subscriptionId) {

+ 35 - 0
services/web/app/src/Features/Subscription/SubscriptionGroupController.mjs

@@ -18,6 +18,7 @@ import {
   ManuallyCollectedError,
   ManuallyCollectedError,
   PendingChangeError,
   PendingChangeError,
   InactiveError,
   InactiveError,
+  SubtotalLimitExceededError,
 } from './Errors.js'
 } from './Errors.js'
 import RecurlyClient from './RecurlyClient.js'
 import RecurlyClient from './RecurlyClient.js'
 
 
@@ -205,6 +206,13 @@ async function previewAddSeatsSubscriptionChange(req, res) {
       return res.status(422).end()
       return res.status(422).end()
     }
     }
 
 
+    if (error instanceof SubtotalLimitExceededError) {
+      return res.status(422).json({
+        code: 'subtotal_limit_exceeded',
+        adding: req.body.adding,
+      })
+    }
+
     logger.err(
     logger.err(
       { error },
       { error },
       'error trying to preview "add seats" subscription change'
       'error trying to preview "add seats" subscription change'
@@ -239,6 +247,13 @@ async function createAddSeatsSubscriptionChange(req, res) {
       return res.status(422).end()
       return res.status(422).end()
     }
     }
 
 
+    if (error instanceof SubtotalLimitExceededError) {
+      return res.status(422).json({
+        code: 'subtotal_limit_exceeded',
+        adding: req.body.adding,
+      })
+    }
+
     logger.err(
     logger.err(
       { error },
       { error },
       'error trying to create "add seats" subscription change'
       'error trying to create "add seats" subscription change'
@@ -313,6 +328,10 @@ async function subscriptionUpgradePage(req, res) {
       )
       )
     }
     }
 
 
+    if (error instanceof SubtotalLimitExceededError) {
+      return res.redirect('/user/subscription/group/subtotal-limit-exceeded')
+    }
+
     if (error instanceof PendingChangeError || error instanceof InactiveError) {
     if (error instanceof PendingChangeError || error instanceof InactiveError) {
       return res.redirect('/user/subscription')
       return res.redirect('/user/subscription')
     }
     }
@@ -370,6 +389,21 @@ async function manuallyCollectedSubscription(req, res) {
   }
   }
 }
 }
 
 
+async function subtotalLimitExceeded(req, res) {
+  try {
+    const userId = SessionManager.getLoggedInUserId(req.session)
+    const subscription =
+      await SubscriptionLocator.promises.getUsersSubscription(userId)
+
+    res.render('subscriptions/subtotal-limit-exceeded', {
+      groupName: subscription.teamName,
+    })
+  } catch (error) {
+    logger.err({ error }, 'error trying to render subtotal limit exceeded page')
+    return res.render('/user/subscription')
+  }
+}
+
 export default {
 export default {
   removeUserFromGroup: expressify(removeUserFromGroup),
   removeUserFromGroup: expressify(removeUserFromGroup),
   removeSelfFromGroup: expressify(removeSelfFromGroup),
   removeSelfFromGroup: expressify(removeSelfFromGroup),
@@ -386,4 +420,5 @@ export default {
   upgradeSubscription: expressify(upgradeSubscription),
   upgradeSubscription: expressify(upgradeSubscription),
   missingBillingInformation: expressify(missingBillingInformation),
   missingBillingInformation: expressify(missingBillingInformation),
   manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
   manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
+  subtotalLimitExceeded: expressify(subtotalLimitExceeded),
 }
 }

+ 8 - 0
services/web/app/src/Features/Subscription/SubscriptionRouter.mjs

@@ -135,6 +135,14 @@ export default {
       SubscriptionGroupController.manuallyCollectedSubscription
       SubscriptionGroupController.manuallyCollectedSubscription
     )
     )
 
 
+    webRouter.get(
+      '/user/subscription/group/subtotal-limit-exceeded',
+      AuthenticationController.requireLogin(),
+      RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
+      SubscriptionGroupController.flexibleLicensingSplitTest,
+      SubscriptionGroupController.subtotalLimitExceeded
+    )
+
     // Team invites
     // Team invites
     webRouter.get(
     webRouter.get(
       '/subscription/invites/:token/',
       '/subscription/invites/:token/',

+ 13 - 0
services/web/app/views/subscriptions/subtotal-limit-exceeded.pug

@@ -0,0 +1,13 @@
+extends ../layout-marketing
+
+block vars
+	- bootstrap5PageStatus = 'enabled' // Enforce BS5 version
+
+block entrypointVar
+	- entrypoint = 'pages/user/subscription/group-management/subtotal-limit-exceeded'
+
+block append meta
+	meta(name="ol-groupName", data-type="string", content=groupName)
+
+block content
+	main.content.content-alt#subtotal-limit-exceeded-root

+ 2 - 0
services/web/frontend/extracted-translations.json

@@ -1539,6 +1539,8 @@
   "somthing_went_wrong_compiling": "",
   "somthing_went_wrong_compiling": "",
   "sorry_it_looks_like_that_didnt_work_this_time": "",
   "sorry_it_looks_like_that_didnt_work_this_time": "",
   "sorry_there_are_no_experiments": "",
   "sorry_there_are_no_experiments": "",
+  "sorry_there_was_an_issue_adding_x_users_to_your_subscription": "",
+  "sorry_there_was_an_issue_upgrading_your_subscription": "",
   "sorry_your_table_cant_be_displayed_at_the_moment": "",
   "sorry_your_table_cant_be_displayed_at_the_moment": "",
   "sort_by": "",
   "sort_by": "",
   "sort_by_x": "",
   "sort_by_x": "",

+ 24 - 2
services/web/frontend/js/features/group-management/components/add-seats/add-seats.tsx

@@ -20,7 +20,7 @@ import CostSummary from '@/features/group-management/components/add-seats/cost-s
 import RequestStatus from '@/features/group-management/components/request-status'
 import RequestStatus from '@/features/group-management/components/request-status'
 import useAsync from '@/shared/hooks/use-async'
 import useAsync from '@/shared/hooks/use-async'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
-import { postJSON } from '@/infrastructure/fetch-json'
+import { FetchError, postJSON } from '@/infrastructure/fetch-json'
 import { debugConsole } from '@/utils/debugging'
 import { debugConsole } from '@/utils/debugging'
 import * as yup from 'yup'
 import * as yup from 'yup'
 import {
 import {
@@ -54,7 +54,8 @@ function AddSeats() {
     runAsync: runAsyncCostSummary,
     runAsync: runAsyncCostSummary,
     data: costSummaryData,
     data: costSummaryData,
     reset: resetCostSummaryData,
     reset: resetCostSummaryData,
-  } = useAsync<CostSummaryData>()
+    error: errorCostSummary,
+  } = useAsync<CostSummaryData, FetchError>()
   const {
   const {
     isLoading: isAddingSeats,
     isLoading: isAddingSeats,
     isError: isErrorAddingSeats,
     isError: isErrorAddingSeats,
@@ -326,6 +327,7 @@ function AddSeats() {
                 <CostSummarySection
                 <CostSummarySection
                   isLoadingCostSummary={isLoadingCostSummary}
                   isLoadingCostSummary={isLoadingCostSummary}
                   isErrorCostSummary={isErrorCostSummary}
                   isErrorCostSummary={isErrorCostSummary}
+                  errorCostSummary={errorCostSummary}
                   shouldContactSales={shouldContactSales}
                   shouldContactSales={shouldContactSales}
                   costSummaryData={costSummaryData}
                   costSummaryData={costSummaryData}
                   totalLicenses={totalLicenses}
                   totalLicenses={totalLicenses}
@@ -379,6 +381,7 @@ function AddSeats() {
 type CostSummarySectionProps = {
 type CostSummarySectionProps = {
   isLoadingCostSummary: boolean
   isLoadingCostSummary: boolean
   isErrorCostSummary: boolean
   isErrorCostSummary: boolean
+  errorCostSummary: Nullable<FetchError>
   shouldContactSales: boolean
   shouldContactSales: boolean
   costSummaryData: Nullable<CostSummaryData>
   costSummaryData: Nullable<CostSummaryData>
   totalLicenses: number
   totalLicenses: number
@@ -387,6 +390,7 @@ type CostSummarySectionProps = {
 function CostSummarySection({
 function CostSummarySection({
   isLoadingCostSummary,
   isLoadingCostSummary,
   isErrorCostSummary,
   isErrorCostSummary,
+  errorCostSummary,
   shouldContactSales,
   shouldContactSales,
   costSummaryData,
   costSummaryData,
   totalLicenses,
   totalLicenses,
@@ -416,6 +420,24 @@ function CostSummarySection({
   }
   }
 
 
   if (isErrorCostSummary) {
   if (isErrorCostSummary) {
+    if (errorCostSummary?.data?.code === 'subtotal_limit_exceeded') {
+      return (
+        <Notification
+          type="error"
+          content={
+            <Trans
+              i18nKey="sorry_there_was_an_issue_adding_x_users_to_your_subscription"
+              // eslint-disable-next-line react/jsx-key, jsx-a11y/anchor-has-content
+              components={[<a href="/contact" rel="noreferrer noopener" />]}
+              values={{ count: errorCostSummary?.data?.adding }}
+              shouldUnescape
+              tOptions={{ interpolation: { escapeValue: true } }}
+            />
+          }
+        />
+      )
+    }
+
     return (
     return (
       <Notification type="error" content={t('generic_something_went_wrong')} />
       <Notification type="error" content={t('generic_something_went_wrong')} />
     )
     )

+ 25 - 0
services/web/frontend/js/features/group-management/components/subtotal-limit-exceeded.tsx

@@ -0,0 +1,25 @@
+import { Trans } from 'react-i18next'
+import OLNotification from '@/features/ui/components/ol/ol-notification'
+import Card from '@/features/group-management/components/card'
+
+function SubtotalLimitExceeded() {
+  return (
+    <Card>
+      <OLNotification
+        type="error"
+        content={
+          <Trans
+            i18nKey="sorry_there_was_an_issue_upgrading_your_subscription"
+            components={[
+              // eslint-disable-next-line jsx-a11y/anchor-has-content, react/jsx-key
+              <a href="/contact" rel="noreferrer noopener" />,
+            ]}
+          />
+        }
+        className="m-0"
+      />
+    </Card>
+  )
+}
+
+export default SubtotalLimitExceeded

+ 8 - 0
services/web/frontend/js/pages/user/subscription/group-management/subtotal-limit-exceeded.tsx

@@ -0,0 +1,8 @@
+import '../base'
+import ReactDOM from 'react-dom'
+import SubtotalLimitExceeded from '@/features/group-management/components/subtotal-limit-exceeded'
+
+const element = document.getElementById('subtotal-limit-exceeded-root')
+if (element) {
+  ReactDOM.render(<SubtotalLimitExceeded />, element)
+}

+ 2 - 0
services/web/locales/en.json

@@ -2016,6 +2016,8 @@
   "sorry_it_looks_like_that_didnt_work_this_time": "Sorry! It looks like that didn’t work this time. Please try again.",
   "sorry_it_looks_like_that_didnt_work_this_time": "Sorry! It looks like that didn’t work this time. Please try again.",
   "sorry_something_went_wrong_opening_the_document_please_try_again": "Sorry, an unexpected error occurred when trying to open this content on Overleaf. Please try again.",
   "sorry_something_went_wrong_opening_the_document_please_try_again": "Sorry, an unexpected error occurred when trying to open this content on Overleaf. Please try again.",
   "sorry_there_are_no_experiments": "Sorry, there are no experiments currently running in Overleaf Labs.",
   "sorry_there_are_no_experiments": "Sorry, there are no experiments currently running in Overleaf Labs.",
+  "sorry_there_was_an_issue_adding_x_users_to_your_subscription": "Sorry, there was an issue adding __count__ users to your subscription. Please <0>contact our Support team</0> for help.",
+  "sorry_there_was_an_issue_upgrading_your_subscription": "Sorry, there was an issue upgrading your subscription. Please <0>contact our Support team</0> for help.",
   "sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.",
   "sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.",
   "sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.",
   "sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table can’t be displayed at the moment.",
   "sorry_your_token_expired": "Sorry, your token expired",
   "sorry_your_token_expired": "Sorry, your token expired",

+ 24 - 0
services/web/test/frontend/features/group-management/components/subtotal-limit-exceeded.spec.tsx

@@ -0,0 +1,24 @@
+import '../../../helpers/bootstrap-5'
+import SubtotalLimitExceeded from '@/features/group-management/components/subtotal-limit-exceeded'
+
+describe('<SubtotalLimitExceeded />', function () {
+  beforeEach(function () {
+    cy.window().then(win => {
+      win.metaAttributesCache.set('ol-groupName', 'My Awesome Team')
+    })
+
+    cy.mount(<SubtotalLimitExceeded />)
+  })
+
+  it('shows subtotal limit exceeded notification', function () {
+    cy.findByRole('alert').within(() => {
+      cy.findByText(
+        /sorry, there was an issue upgrading your subscription\. Please.*for help/i
+      ).within(() => {
+        cy.findByRole('link', {
+          name: /contact our support team/i,
+        }).should('have.attr', 'href', '/contact')
+      })
+    })
+  })
+})

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

@@ -111,6 +111,7 @@ describe('RecurlyClient', function () {
     }
     }
     this.Errors = {
     this.Errors = {
       MissingBillingInfoError: class extends Error {},
       MissingBillingInfoError: class extends Error {},
+      SubtotalLimitExceededError: class extends Error {},
     }
     }
 
 
     return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
     return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
@@ -308,6 +309,32 @@ describe('RecurlyClient', function () {
         })
         })
       ).to.eventually.be.rejectedWith(Error)
       ).to.eventually.be.rejectedWith(Error)
     })
     })
+
+    it('should throw SubtotalLimitExceededError', async function () {
+      class ValidationError extends recurly.errors.ValidationError {
+        constructor() {
+          super()
+          this.params = [{ param: 'subtotal_amount_in_cents' }]
+        }
+      }
+      this.client.createSubscriptionChange = sinon
+        .stub()
+        .throws(new ValidationError())
+      await expect(
+        this.RecurlyClient.promises.applySubscriptionChangeRequest({
+          subscription: this.subscription,
+        })
+      ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
+    })
+
+    it('should rethrow errors different than SubtotalLimitExceededError', async function () {
+      this.client.createSubscriptionChange = sinon.stub().throws(new Error())
+      await expect(
+        this.RecurlyClient.promises.applySubscriptionChangeRequest({
+          subscription: this.subscription,
+        })
+      ).to.be.rejectedWith(Error)
+    })
   })
   })
 
 
   describe('removeSubscriptionChange', function () {
   describe('removeSubscriptionChange', function () {
@@ -467,6 +494,40 @@ describe('RecurlyClient', function () {
         expect(immediateCharge.total).to.be.equal(96.2)
         expect(immediateCharge.total).to.be.equal(96.2)
       })
       })
     })
     })
+
+    it('should throw SubtotalLimitExceededError', async function () {
+      class ValidationError extends recurly.errors.ValidationError {
+        constructor() {
+          super()
+          this.params = [{ param: 'subtotal_amount_in_cents' }]
+        }
+      }
+      this.client.previewSubscriptionChange = sinon
+        .stub()
+        .throws(new ValidationError())
+      await expect(
+        this.RecurlyClient.promises.previewSubscriptionChange(
+          new RecurlySubscriptionChangeRequest({
+            subscription: this.subscription,
+            timeframe: 'now',
+            planCode: 'new-plan',
+          })
+        )
+      ).to.be.rejectedWith(this.Errors.SubtotalLimitExceededError)
+    })
+
+    it('should rethrow errors different than SubtotalLimitExceededError', async function () {
+      this.client.previewSubscriptionChange = sinon.stub().throws(new Error())
+      await expect(
+        this.RecurlyClient.promises.previewSubscriptionChange(
+          new RecurlySubscriptionChangeRequest({
+            subscription: this.subscription,
+            timeframe: 'now',
+            planCode: 'new-plan',
+          })
+        )
+      ).to.be.rejectedWith(Error)
+    })
   })
   })
 
 
   describe('getPaymentMethod', function () {
   describe('getPaymentMethod', function () {

+ 64 - 0
services/web/test/unit/src/Subscription/SubscriptionGroupControllerTests.mjs

@@ -128,6 +128,7 @@ describe('SubscriptionGroupController', function () {
       ManuallyCollectedError: class extends Error {},
       ManuallyCollectedError: class extends Error {},
       PendingChangeError: class extends Error {},
       PendingChangeError: class extends Error {},
       InactiveError: class extends Error {},
       InactiveError: class extends Error {},
+      SubtotalLimitExceededError: class extends Error {},
     }
     }
 
 
     this.Controller = await esmock.strict(modulePath, {
     this.Controller = await esmock.strict(modulePath, {
@@ -495,6 +496,30 @@ describe('SubscriptionGroupController', function () {
 
 
       this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
       this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
     })
     })
+
+    it('should fail previewing "add seats" change with SubtotalLimitExceededError', function (done) {
+      this.req.body = { adding: 2 }
+      this.SubscriptionGroupHandler.promises.previewAddSeatsSubscriptionChange =
+        sinon.stub().throws(new this.Errors.SubtotalLimitExceededError())
+
+      const res = {
+        status: statusCode => {
+          statusCode.should.equal(422)
+
+          return {
+            json: data => {
+              data.should.deep.equal({
+                code: 'subtotal_limit_exceeded',
+                adding: this.req.body.adding,
+              })
+              done()
+            },
+          }
+        },
+      }
+
+      this.Controller.previewAddSeatsSubscriptionChange(this.req, res)
+    })
   })
   })
 
 
   describe('createAddSeatsSubscriptionChange', function () {
   describe('createAddSeatsSubscriptionChange', function () {
@@ -532,6 +557,30 @@ describe('SubscriptionGroupController', function () {
 
 
       this.Controller.createAddSeatsSubscriptionChange(this.req, res)
       this.Controller.createAddSeatsSubscriptionChange(this.req, res)
     })
     })
+
+    it('should fail applying "add seats" change with SubtotalLimitExceededError', function (done) {
+      this.req.body = { adding: 2 }
+      this.SubscriptionGroupHandler.promises.createAddSeatsSubscriptionChange =
+        sinon.stub().throws(new this.Errors.SubtotalLimitExceededError())
+
+      const res = {
+        status: statusCode => {
+          statusCode.should.equal(422)
+
+          return {
+            json: data => {
+              data.should.deep.equal({
+                code: 'subtotal_limit_exceeded',
+                adding: this.req.body.adding,
+              })
+              done()
+            },
+          }
+        },
+      }
+
+      this.Controller.createAddSeatsSubscriptionChange(this.req, res)
+    })
   })
   })
 
 
   describe('submitForm', function () {
   describe('submitForm', function () {
@@ -666,6 +715,21 @@ describe('SubscriptionGroupController', function () {
 
 
       this.Controller.subscriptionUpgradePage(this.req, res)
       this.Controller.subscriptionUpgradePage(this.req, res)
     })
     })
+
+    it('should redirect to subtotal limit exceeded page', function (done) {
+      this.SubscriptionGroupHandler.promises.getGroupPlanUpgradePreview = sinon
+        .stub()
+        .throws(new this.Errors.SubtotalLimitExceededError())
+
+      const res = {
+        redirect: url => {
+          url.should.equal('/user/subscription/group/subtotal-limit-exceeded')
+          done()
+        },
+      }
+
+      this.Controller.subscriptionUpgradePage(this.req, res)
+    })
   })
   })
 
 
   describe('upgradeSubscription', function () {
   describe('upgradeSubscription', function () {