Quellcode durchsuchen

Merge pull request #17005 from overleaf/dp-mongoose-callback-subscription-handler

Promisify SubscriptionHandler and SubscriptionHandlerTests

GitOrigin-RevId: b34328ee2cca4449a02723a587a1bfb887ed847a
David vor 2 Jahren
Ursprung
Commit
6551aba1a5

+ 11 - 0
services/web/app/src/Features/Subscription/RecurlyWrapper.js

@@ -1041,7 +1041,18 @@ const RecurlyWrapper = {
 }
 
 RecurlyWrapper.promises = {
+  attemptInvoiceCollection: promisify(RecurlyWrapper.attemptInvoiceCollection),
+  createSubscription: promisify(RecurlyWrapper.createSubscription),
+  extendTrial: promisify(RecurlyWrapper.extendTrial),
+  getBillingInfo: promisify(RecurlyWrapper.getBillingInfo),
+  getAccountPastDueInvoices: promisify(
+    RecurlyWrapper.getAccountPastDueInvoices
+  ),
   getSubscription: promisify(RecurlyWrapper.getSubscription),
+  listAccountActiveSubscriptions: promisify(
+    RecurlyWrapper.listAccountActiveSubscriptions
+  ),
+  redeemCoupon: promisify(RecurlyWrapper.redeemCoupon),
   updateAccountEmailAddress: promisify(updateAccountEmailAddress),
 }
 

+ 158 - 228
services/web/app/src/Features/Subscription/SubscriptionHandler.js

@@ -1,5 +1,3 @@
-const async = require('async')
-const { promisify } = require('util')
 const RecurlyWrapper = require('./RecurlyWrapper')
 const RecurlyClient = require('./RecurlyClient')
 const { User } = require('../../models/User')
@@ -11,173 +9,120 @@ const PlansLocator = require('./PlansLocator')
 const SubscriptionHelper = require('./SubscriptionHelper')
 const { callbackify } = require('@overleaf/promise-utils')
 
-function validateNoSubscriptionInRecurly(userId, callback) {
-  RecurlyWrapper.listAccountActiveSubscriptions(
-    userId,
-    function (error, subscriptions) {
-      if (!subscriptions) {
-        subscriptions = []
-      }
-      if (error) {
-        return callback(error)
-      }
-      if (subscriptions.length > 0) {
-        SubscriptionUpdater.syncSubscription(
-          subscriptions[0],
-          userId,
-          function (error) {
-            if (error) {
-              return callback(error)
-            }
-            callback(null, false)
-          }
-        )
-      } else {
-        callback(null, true)
-      }
-    }
-  )
-}
+async function validateNoSubscriptionInRecurly(userId) {
+  let subscriptions =
+    await RecurlyWrapper.promises.listAccountActiveSubscriptions(userId)
 
-function createSubscription(
-  user,
-  subscriptionDetails,
-  recurlyTokenIds,
-  callback
-) {
-  validateNoSubscriptionInRecurly(user._id, function (error, valid) {
-    if (error) {
-      return callback(error)
-    }
-    if (!valid) {
-      return callback(new Error('user already has subscription in recurly'))
-    }
-    RecurlyWrapper.createSubscription(
-      user,
-      subscriptionDetails,
-      recurlyTokenIds,
-      function (error, recurlySubscription) {
-        if (error) {
-          return callback(error)
-        }
-        SubscriptionUpdater.syncSubscription(
-          recurlySubscription,
-          user._id,
-          function (error) {
-            if (error) {
-              return callback(error)
-            }
-            callback()
-          }
-        )
-      }
+  if (!subscriptions) {
+    subscriptions = []
+  }
+
+  if (subscriptions.length > 0) {
+    await SubscriptionUpdater.promises.syncSubscription(
+      subscriptions[0],
+      userId
     )
-  })
+
+    return false
+  }
+
+  return true
 }
 
-function updateSubscription(user, planCode, couponCode, callback) {
-  LimitationsManager.userHasV2Subscription(
+async function createSubscription(user, subscriptionDetails, recurlyTokenIds) {
+  const valid = await validateNoSubscriptionInRecurly(user._id)
+
+  if (!valid) {
+    throw new Error('user already has subscription in recurly')
+  }
+
+  const recurlySubscription = await RecurlyWrapper.promises.createSubscription(
     user,
-    function (err, hasSubscription, subscription) {
-      if (err) {
-        logger.warn(
-          { err, userId: user._id, hasSubscription },
-          'there was an error checking user v2 subscription'
-        )
-      }
-      if (!hasSubscription) {
-        callback()
-      } else {
-        async.series(
-          [
-            function (cb) {
-              if (!couponCode) {
-                return cb()
-              }
-              RecurlyWrapper.getSubscription(
-                subscription.recurlySubscription_id,
-                { includeAccount: true },
-                function (err, usersSubscription) {
-                  if (err) {
-                    return cb(err)
-                  }
-                  RecurlyWrapper.redeemCoupon(
-                    usersSubscription.account.account_code,
-                    couponCode,
-                    cb
-                  )
-                }
-              )
-            },
-            function (cb) {
-              let changeAtTermEnd
-              const currentPlan = PlansLocator.findLocalPlanInSettings(
-                subscription.planCode
-              )
-              const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
-              if (currentPlan && newPlan) {
-                changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
-                  currentPlan,
-                  newPlan
-                )
-              } else {
-                logger.error(
-                  { currentPlan: subscription.planCode, newPlan: planCode },
-                  'unable to locate both plans in settings'
-                )
-                return cb(new Error('unable to locate both plans in settings'))
-              }
-              const timeframe = changeAtTermEnd ? 'term_end' : 'now'
-              RecurlyClient.changeSubscriptionByUuid(
-                subscription.recurlySubscription_id,
-                { planCode, timeframe },
-                function (error, subscriptionChange) {
-                  if (error) {
-                    return cb(error)
-                  }
-                  // v2 recurly API wants a UUID, but UUID isn't included in the subscription change response
-                  // we got the UUID from the DB using userHasV2Subscription() - it is the only property
-                  // we need to be able to build a 'recurlySubscription' object for syncSubscription()
-                  syncSubscription(
-                    { uuid: subscription.recurlySubscription_id },
-                    user._id,
-                    cb
-                  )
-                }
-              )
-            },
-          ],
-          callback
-        )
-      }
-    }
+    subscriptionDetails,
+    recurlyTokenIds
+  )
+
+  await SubscriptionUpdater.promises.syncSubscription(
+    recurlySubscription,
+    user._id
   )
 }
 
-function cancelPendingSubscriptionChange(user, callback) {
-  LimitationsManager.userHasV2Subscription(
-    user,
-    function (err, hasSubscription, subscription) {
-      if (err) {
-        return callback(err)
-      }
-      if (hasSubscription) {
-        RecurlyClient.removeSubscriptionChangeByUuid(
-          subscription.recurlySubscription_id,
-          function (error) {
-            if (error) {
-              return callback(error)
-            }
-            callback()
-          }
-        )
-      } else {
-        callback()
-      }
-    }
+async function updateSubscription(user, planCode, couponCode) {
+  let hasSubscription = false
+  let subscription
+
+  try {
+    ;({ hasSubscription, subscription } =
+      await LimitationsManager.promises.userHasV2Subscription(user))
+  } catch (err) {
+    logger.warn(
+      { err, userId: user._id },
+      'there was an error checking user v2 subscription'
+    )
+  }
+
+  if (!hasSubscription) {
+    return
+  }
+
+  if (couponCode) {
+    const usersSubscription = await RecurlyWrapper.promises.getSubscription(
+      subscription.recurlySubscription_id,
+      { includeAccount: true }
+    )
+
+    await RecurlyWrapper.promises.redeemCoupon(
+      usersSubscription.account.account_code,
+      couponCode
+    )
+  }
+
+  let changeAtTermEnd
+  const currentPlan = PlansLocator.findLocalPlanInSettings(
+    subscription.planCode
+  )
+  const newPlan = PlansLocator.findLocalPlanInSettings(planCode)
+  if (currentPlan && newPlan) {
+    changeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
+      currentPlan,
+      newPlan
+    )
+  } else {
+    logger.error(
+      { currentPlan: subscription.planCode, newPlan: planCode },
+      'unable to locate both plans in settings'
+    )
+    throw new Error('unable to locate both plans in settings')
+  }
+
+  const timeframe = changeAtTermEnd ? 'term_end' : 'now'
+
+  await RecurlyClient.promises.changeSubscriptionByUuid(
+    subscription.recurlySubscription_id,
+    { planCode, timeframe }
+  )
+
+  // v2 recurly API wants a UUID, but UUID isn't included in the subscription change response
+  // we got the UUID from the DB using userHasV2Subscription() - it is the only property
+  // we need to be able to build a 'recurlySubscription' object for syncSubscription()
+  await syncSubscription(
+    { uuid: subscription.recurlySubscription_id },
+    user._id
   )
 }
 
+async function cancelPendingSubscriptionChange(user) {
+  const { hasSubscription, subscription } =
+    await LimitationsManager.promises.userHasV2Subscription(user)
+
+  if (hasSubscription) {
+    await RecurlyClient.promises.removeSubscriptionChangeByUuid(
+      subscription.recurlySubscription_id
+    )
+  }
+}
+
 async function cancelSubscription(user) {
   try {
     const { hasSubscription, subscription } =
@@ -236,33 +181,24 @@ async function reactivateSubscription(user) {
   }
 }
 
-function syncSubscription(recurlySubscription, requesterData, callback) {
-  RecurlyWrapper.getSubscription(
+async function syncSubscription(recurlySubscription, requesterData) {
+  const storedSubscription = await RecurlyWrapper.promises.getSubscription(
     recurlySubscription.uuid,
-    { includeAccount: true },
-    function (error, recurlySubscription) {
-      if (error) {
-        return callback(error)
-      }
-      User.findById(
-        recurlySubscription.account.account_code,
-        { _id: 1 },
-        function (error, user) {
-          if (error) {
-            return callback(error)
-          }
-          if (!user) {
-            return callback(new Error('no user found'))
-          }
-          SubscriptionUpdater.syncSubscription(
-            recurlySubscription,
-            user._id,
-            requesterData,
-            callback
-          )
-        }
-      )
-    }
+    { includeAccount: true }
+  )
+
+  const user = await User.findById(storedSubscription.account.account_code, {
+    _id: 1,
+  }).exec()
+
+  if (!user) {
+    throw new Error('no user found')
+  }
+
+  await SubscriptionUpdater.promises.syncSubscription(
+    storedSubscription,
+    user._id,
+    requesterData
   )
 }
 
@@ -270,39 +206,33 @@ function syncSubscription(recurlySubscription, requesterData, callback) {
 // customer is using Paypal and b) there is only one past due invoice.
 // This is used because Recurly doesn't always attempt collection of paast due
 // invoices after Paypal billing info were updated.
-function attemptPaypalInvoiceCollection(recurlyAccountCode, callback) {
-  RecurlyWrapper.getBillingInfo(recurlyAccountCode, (error, billingInfo) => {
-    if (error) {
-      return callback(error)
-    }
-    if (!billingInfo.paypal_billing_agreement_id) {
-      // this is not a Paypal user
-      return callback()
-    }
-    RecurlyWrapper.getAccountPastDueInvoices(
-      recurlyAccountCode,
-      (error, pastDueInvoices) => {
-        if (error) {
-          return callback(error)
-        }
-        if (pastDueInvoices.length !== 1) {
-          // no past due invoices, or more than one. Ignore.
-          return callback()
-        }
-        RecurlyWrapper.attemptInvoiceCollection(
-          pastDueInvoices[0].invoice_number,
-          callback
-        )
-      }
-    )
-  })
+async function attemptPaypalInvoiceCollection(recurlyAccountCode) {
+  const billingInfo = await RecurlyWrapper.promises.getBillingInfo(
+    recurlyAccountCode
+  )
+
+  if (!billingInfo.paypal_billing_agreement_id) {
+    // this is not a Paypal user
+    return
+  }
+
+  const pastDueInvoices =
+    await RecurlyWrapper.promises.getAccountPastDueInvoices(recurlyAccountCode)
+
+  if (pastDueInvoices.length !== 1) {
+    // no past due invoices, or more than one. Ignore.
+    return
+  }
+
+  return await RecurlyWrapper.promises.attemptInvoiceCollection(
+    pastDueInvoices[0].invoice_number
+  )
 }
 
-function extendTrial(subscription, daysToExend, callback) {
-  RecurlyWrapper.extendTrial(
+async function extendTrial(subscription, daysToExend) {
+  await RecurlyWrapper.promises.extendTrial(
     subscription.recurlySubscription_id,
-    daysToExend,
-    callback
+    daysToExend
   )
 }
 
@@ -318,24 +248,24 @@ async function _updateSubscriptionFromRecurly(subscription) {
 }
 
 module.exports = {
-  validateNoSubscriptionInRecurly,
-  createSubscription,
-  updateSubscription,
-  cancelPendingSubscriptionChange,
+  validateNoSubscriptionInRecurly: callbackify(validateNoSubscriptionInRecurly),
+  createSubscription: callbackify(createSubscription),
+  updateSubscription: callbackify(updateSubscription),
+  cancelPendingSubscriptionChange: callbackify(cancelPendingSubscriptionChange),
   cancelSubscription: callbackify(cancelSubscription),
   reactivateSubscription: callbackify(reactivateSubscription),
-  syncSubscription,
-  attemptPaypalInvoiceCollection,
-  extendTrial,
+  syncSubscription: callbackify(syncSubscription),
+  attemptPaypalInvoiceCollection: callbackify(attemptPaypalInvoiceCollection),
+  extendTrial: callbackify(extendTrial),
   promises: {
-    validateNoSubscriptionInRecurly: promisify(validateNoSubscriptionInRecurly),
-    createSubscription: promisify(createSubscription),
-    updateSubscription: promisify(updateSubscription),
-    cancelPendingSubscriptionChange: promisify(cancelPendingSubscriptionChange),
+    validateNoSubscriptionInRecurly,
+    createSubscription,
+    updateSubscription,
+    cancelPendingSubscriptionChange,
     cancelSubscription,
     reactivateSubscription,
-    syncSubscription: promisify(syncSubscription),
-    attemptPaypalInvoiceCollection: promisify(attemptPaypalInvoiceCollection),
-    extendTrial: promisify(extendTrial),
+    syncSubscription,
+    attemptPaypalInvoiceCollection,
+    extendTrial,
   },
 }

+ 182 - 188
services/web/test/unit/src/Subscription/SubscriptionHandlerTests.js

@@ -48,8 +48,6 @@ const mockSubscriptionChanges = {
 
 describe('SubscriptionHandler', function () {
   beforeEach(function () {
-    this.callback = sinon.stub()
-
     this.Settings = {
       plans: [
         {
@@ -78,50 +76,42 @@ describe('SubscriptionHandler', function () {
       recurlySubscription_id: this.activeRecurlySubscription.uuid,
     }
     this.RecurlyWrapper = {
-      getSubscription: sinon
-        .stub()
-        .callsArgWith(2, null, this.activeRecurlySubscription),
-      redeemCoupon: sinon.stub().callsArgWith(2),
-      createSubscription: sinon
-        .stub()
-        .callsArgWith(3, null, this.activeRecurlySubscription),
-      getBillingInfo: sinon.stub().yields(),
-      getAccountPastDueInvoices: sinon.stub().yields(),
-      attemptInvoiceCollection: sinon.stub().yields(),
-      listAccountActiveSubscriptions: sinon.stub().yields(null, []),
       promises: {
         getSubscription: sinon.stub().resolves(this.activeRecurlySubscription),
+        redeemCoupon: sinon.stub().resolves(),
+        createSubscription: sinon
+          .stub()
+          .resolves(this.activeRecurlySubscription),
+        getBillingInfo: sinon.stub().resolves(),
+        getAccountPastDueInvoices: sinon.stub().resolves(),
+        attemptInvoiceCollection: sinon.stub().resolves(),
+        listAccountActiveSubscriptions: sinon.stub().resolves([]),
       },
     }
     this.RecurlyClient = {
-      changeSubscriptionByUuid: sinon
-        .stub()
-        .yields(null, this.activeRecurlySubscriptionChange),
-      getSubscription: sinon
-        .stub()
-        .yields(null, this.activeRecurlyClientSubscription),
-      reactivateSubscriptionByUuid: sinon
-        .stub()
-        .yields(null, this.activeRecurlyClientSubscription),
-      cancelSubscriptionByUuid: sinon.stub().yields(),
       promises: {
         reactivateSubscriptionByUuid: sinon
           .stub()
           .resolves(this.activeRecurlyClientSubscription),
         cancelSubscriptionByUuid: sinon.stub().resolves(),
+        changeSubscriptionByUuid: sinon
+          .stub()
+          .resolves(this.activeRecurlySubscriptionChange),
+        getSubscription: sinon
+          .stub()
+          .resolves(this.activeRecurlyClientSubscription),
       },
     }
 
     this.SubscriptionUpdater = {
-      syncSubscription: sinon.stub().yields(),
-      startFreeTrial: sinon.stub().callsArgWith(1),
       promises: {
         updateSubscriptionFromRecurly: sinon.stub().resolves(),
+        syncSubscription: sinon.stub().resolves(),
+        startFreeTrial: sinon.stub().resolves(),
       },
     }
 
     this.LimitationsManager = {
-      userHasV2Subscription: sinon.stub(),
       promises: {
         userHasV2Subscription: sinon.stub().resolves(),
       },
@@ -132,8 +122,6 @@ describe('SubscriptionHandler', function () {
       sendDeferredEmail: sinon.stub(),
     }
 
-    this.AnalyticsManager = { recordEventForUser: sinon.stub() }
-
     this.PlansLocator = {
       findLocalPlanInSettings: sinon.stub().returns({ planCode: 'plan' }),
     }
@@ -170,27 +158,28 @@ describe('SubscriptionHandler', function () {
     })
 
     describe('successfully', function () {
-      beforeEach(function () {
-        this.SubscriptionHandler.createSubscription(
+      beforeEach(async function () {
+        await this.SubscriptionHandler.promises.createSubscription(
           this.user,
           this.subscriptionDetails,
-          this.recurlyTokenIds,
-          this.callback
+          this.recurlyTokenIds
         )
       })
 
       it('should create the subscription with the wrapper', function () {
-        this.RecurlyWrapper.createSubscription
+        this.RecurlyWrapper.promises.createSubscription
           .calledWith(this.user, this.subscriptionDetails, this.recurlyTokenIds)
           .should.equal(true)
       })
 
       it('should sync the subscription to the user', function () {
-        this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
-        this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
+        this.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal(
+          true
+        )
+        this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
           this.activeRecurlySubscription
         )
-        this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
+        this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
           this.user._id
         )
       })
@@ -198,21 +187,19 @@ describe('SubscriptionHandler', function () {
 
     describe('when there is already a subscription in Recurly', function () {
       beforeEach(function () {
-        this.RecurlyWrapper.listAccountActiveSubscriptions.yields(null, [
+        this.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([
           this.subscription,
         ])
-        this.SubscriptionHandler.createSubscription(
-          this.user,
-          this.subscriptionDetails,
-          this.recurlyTokenIds,
-          this.callback
-        )
       })
 
-      it('should  an error', function () {
-        this.callback.calledWith(
-          new Error('user already has subscription in recurly')
-        )
+      it('should an error', function () {
+        expect(
+          this.SubscriptionHandler.promises.createSubscription(
+            this.user,
+            this.subscriptionDetails,
+            this.recurlyTokenIds
+          )
+        ).to.be.rejectedWith('user already has subscription in recurly')
       })
     })
   })
@@ -220,21 +207,23 @@ describe('SubscriptionHandler', function () {
   function shouldUpdateSubscription() {
     it('should update the subscription', function () {
       expect(
-        this.RecurlyClient.changeSubscriptionByUuid
+        this.RecurlyClient.promises.changeSubscriptionByUuid
       ).to.have.been.calledWith(this.subscription.recurlySubscription_id)
       const updateOptions =
-        this.RecurlyClient.changeSubscriptionByUuid.args[0][1]
+        this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
       updateOptions.planCode.should.equal(this.plan_code)
     })
   }
 
   function shouldSyncSubscription() {
     it('should sync the new subscription to the user', function () {
-      expect(this.SubscriptionUpdater.syncSubscription).to.have.been.called
-      this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
+      expect(this.SubscriptionUpdater.promises.syncSubscription).to.have.been
+        .called
+
+      this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
         this.activeRecurlySubscription
       )
-      this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
+      this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
         this.user._id
       )
     })
@@ -244,27 +233,26 @@ describe('SubscriptionHandler', function () {
     describe(
       'when change should happen with timeframe ' + timeframe,
       function () {
-        beforeEach(function (done) {
+        beforeEach(async function () {
           this.user.id = this.activeRecurlySubscription.account.account_code
-          this.User.findById = (userId, projection, callback) => {
-            userId.should.equal(this.user.id)
-            callback(null, this.user)
-          }
+          this.User.findById = (userId, projection) => ({
+            exec: () => {
+              userId.should.equal(this.user.id)
+              return Promise.resolve(this.user)
+            },
+          })
           this.plan_code = 'collaborator'
           this.SubscriptionHelper.shouldPlanChangeAtTermEnd.returns(
             shouldPlanChangeAtTermEnd
           )
-          this.LimitationsManager.userHasV2Subscription.callsArgWith(
-            1,
-            null,
-            true,
-            this.subscription
-          )
-          this.SubscriptionHandler.updateSubscription(
+          this.LimitationsManager.promises.userHasV2Subscription.resolves({
+            hasSubscription: true,
+            subscription: this.subscription,
+          })
+          await this.SubscriptionHandler.promises.updateSubscription(
             this.user,
             this.plan_code,
-            null,
-            done
+            null
           )
         })
 
@@ -273,7 +261,7 @@ describe('SubscriptionHandler', function () {
 
         it('should update with timeframe ' + timeframe, function () {
           const updateOptions =
-            this.RecurlyClient.changeSubscriptionByUuid.args[0][1]
+            this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
           updateOptions.timeframe.should.equal(timeframe)
         })
       }
@@ -286,106 +274,102 @@ describe('SubscriptionHandler', function () {
       testUserWithASubscription(true, 'term_end')
 
       describe('when plan(s) could not be located in settings', function () {
-        beforeEach(function () {
+        beforeEach(async function () {
           this.user.id = this.activeRecurlySubscription.account.account_code
-          this.User.findById = (userId, projection, callback) => {
-            userId.should.equal(this.user.id)
-            callback(null, this.user)
-          }
+          this.User.findById = (userId, projection) => ({
+            exec: () => {
+              userId.should.equal(this.user.id)
+              return Promise.resolve(this.user)
+            },
+          })
+
           this.plan_code = 'collaborator'
           this.PlansLocator.findLocalPlanInSettings.returns(null)
-          this.LimitationsManager.userHasV2Subscription.callsArgWith(
-            1,
-            null,
-            true,
-            this.subscription
-          )
-          this.SubscriptionHandler.updateSubscription(
-            this.user,
-            this.plan_code,
-            null,
-            this.callback
-          )
-        })
-
-        it('should not update the subscription', function () {
-          this.RecurlyClient.changeSubscriptionByUuid.called.should.equal(false)
+          this.LimitationsManager.promises.userHasV2Subscription.resolves({
+            hasSubscription: true,
+            subscription: this.subscription,
+          })
         })
 
-        it('should return an error to the callback', function () {
-          this.callback
-            .calledWith(sinon.match.instanceOf(Error))
-            .should.equal(true)
+        it('should be rejected and should not update the subscription', function () {
+          expect(
+            this.SubscriptionHandler.promises.updateSubscription(
+              this.user,
+              this.plan_code,
+              null
+            )
+          ).to.be.rejected
+          this.RecurlyClient.promises.changeSubscriptionByUuid.called.should.equal(
+            false
+          )
         })
       })
     })
 
     describe('with a user without a subscription', function () {
-      beforeEach(function (done) {
-        this.LimitationsManager.userHasV2Subscription.callsArgWith(
-          1,
-          null,
-          false
-        )
-        this.SubscriptionHandler.updateSubscription(
+      beforeEach(async function () {
+        this.LimitationsManager.promises.userHasV2Subscription.resolves(false)
+        await this.SubscriptionHandler.promises.updateSubscription(
           this.user,
           this.plan_code,
-          null,
-          done
+          null
         )
       })
 
       it('should redirect to the subscription dashboard', function () {
-        this.RecurlyClient.changeSubscriptionByUuid.called.should.equal(false)
-        this.SubscriptionUpdater.syncSubscription.called.should.equal(false)
+        this.RecurlyClient.promises.changeSubscriptionByUuid.called.should.equal(
+          false
+        )
+        this.SubscriptionUpdater.promises.syncSubscription.called.should.equal(
+          false
+        )
       })
     })
 
     describe('with a coupon code', function () {
-      beforeEach(function (done) {
+      beforeEach(async function () {
         this.user.id = this.activeRecurlySubscription.account.account_code
-        this.User.findById = (userId, projection, callback) => {
-          userId.should.equal(this.user.id)
-          callback(null, this.user)
-        }
+
+        this.User.findById = (userId, projection) => ({
+          exec: () => {
+            userId.should.equal(this.user.id)
+            return Promise.resolve(this.user)
+          },
+        })
         this.plan_code = 'collaborator'
         this.coupon_code = '1231312'
-        this.LimitationsManager.userHasV2Subscription.callsArgWith(
-          1,
-          null,
-          true,
-          this.subscription
-        )
-        this.SubscriptionHandler.updateSubscription(
+        this.LimitationsManager.promises.userHasV2Subscription.resolves({
+          hasSubscription: true,
+          subscription: this.subscription,
+        })
+        await this.SubscriptionHandler.promises.updateSubscription(
           this.user,
           this.plan_code,
-          this.coupon_code,
-          done
+          this.coupon_code
         )
       })
 
       it('should get the users account', function () {
-        this.RecurlyWrapper.getSubscription
+        this.RecurlyWrapper.promises.getSubscription
           .calledWith(this.activeRecurlySubscription.uuid)
           .should.equal(true)
       })
 
-      it('should redeem the coupon', function (done) {
-        this.RecurlyWrapper.redeemCoupon
+      it('should redeem the coupon', function () {
+        this.RecurlyWrapper.promises.redeemCoupon
           .calledWith(
             this.activeRecurlySubscription.account.account_code,
             this.coupon_code
           )
           .should.equal(true)
-        done()
       })
 
       it('should update the subscription', function () {
-        expect(this.RecurlyClient.changeSubscriptionByUuid).to.be.calledWith(
-          this.subscription.recurlySubscription_id
-        )
+        expect(
+          this.RecurlyClient.promises.changeSubscriptionByUuid
+        ).to.be.calledWith(this.subscription.recurlySubscription_id)
         const updateOptions =
-          this.RecurlyClient.changeSubscriptionByUuid.args[0][1]
+          this.RecurlyClient.promises.changeSubscriptionByUuid.args[0][1]
         updateOptions.planCode.should.equal(this.plan_code)
       })
     })
@@ -393,28 +377,28 @@ describe('SubscriptionHandler', function () {
 
   describe('cancelSubscription', function () {
     describe('with a user without a subscription', function () {
-      beforeEach(function (done) {
-        this.LimitationsManager.promises.userHasV2Subscription.callsArgWith(
-          1,
-          null,
-          false,
-          this.subscription
-        )
-        this.SubscriptionHandler.cancelSubscription(this.user, done)
+      beforeEach(async function () {
+        this.LimitationsManager.promises.userHasV2Subscription.resolves({
+          hasSubscription: false,
+          subscription: this.subscription,
+        })
+        await this.SubscriptionHandler.promises.cancelSubscription(this.user)
       })
 
       it('should redirect to the subscription dashboard', function () {
-        this.RecurlyClient.cancelSubscriptionByUuid.called.should.equal(false)
+        this.RecurlyClient.promises.cancelSubscriptionByUuid.called.should.equal(
+          false
+        )
       })
     })
 
     describe('with a user with a subscription', function () {
-      beforeEach(function (done) {
+      beforeEach(async function () {
         this.LimitationsManager.promises.userHasV2Subscription.resolves({
           hasSubscription: true,
           subscription: this.subscription,
         })
-        this.SubscriptionHandler.cancelSubscription(this.user, done)
+        await this.SubscriptionHandler.promises.cancelSubscription(this.user)
       })
 
       it('should cancel the subscription', function () {
@@ -439,16 +423,18 @@ describe('SubscriptionHandler', function () {
 
   describe('reactivateSubscription', function () {
     describe('with a user without a subscription', function () {
-      beforeEach(function (done) {
+      beforeEach(async function () {
         this.LimitationsManager.promises.userHasV2Subscription.resolves({
           hasSubscription: false,
           subscription: this.subscription,
         })
-        this.SubscriptionHandler.reactivateSubscription(this.user, done)
+        await this.SubscriptionHandler.promises.reactivateSubscription(
+          this.user
+        )
       })
 
       it('should redirect to the subscription dashboard', function () {
-        this.RecurlyClient.reactivateSubscriptionByUuid.called.should.equal(
+        this.RecurlyClient.promises.reactivateSubscriptionByUuid.called.should.equal(
           false
         )
       })
@@ -459,12 +445,14 @@ describe('SubscriptionHandler', function () {
     })
 
     describe('with a user with a subscription', function () {
-      beforeEach(function (done) {
+      beforeEach(async function () {
         this.LimitationsManager.promises.userHasV2Subscription.resolves({
           hasSubscription: true,
           subscription: this.subscription,
         })
-        this.SubscriptionHandler.reactivateSubscription(this.user, done)
+        await this.SubscriptionHandler.promises.reactivateSubscription(
+          this.user
+        )
       })
 
       it('should reactivate the subscription', function () {
@@ -487,37 +475,41 @@ describe('SubscriptionHandler', function () {
 
   describe('syncSubscription', function () {
     describe('with an actionable request', function () {
-      beforeEach(function (done) {
+      beforeEach(async function () {
         this.user.id = this.activeRecurlySubscription.account.account_code
 
-        this.User.findById = (userId, projection, callback) => {
-          userId.should.equal(this.user.id)
-          callback(null, this.user)
-        }
-        this.SubscriptionHandler.syncSubscription(
+        this.User.findById = (userId, projection) => ({
+          exec: () => {
+            userId.should.equal(this.user.id)
+            return Promise.resolve(this.user)
+          },
+        })
+
+        await this.SubscriptionHandler.promises.syncSubscription(
           this.activeRecurlySubscription,
-          {},
-          done
+          {}
         )
       })
 
       it('should request the affected subscription from the API', function () {
-        this.RecurlyWrapper.getSubscription
+        this.RecurlyWrapper.promises.getSubscription
           .calledWith(this.activeRecurlySubscription.uuid)
           .should.equal(true)
       })
 
       it('should request the account details of the subscription', function () {
-        const options = this.RecurlyWrapper.getSubscription.args[0][1]
+        const options = this.RecurlyWrapper.promises.getSubscription.args[0][1]
         options.includeAccount.should.equal(true)
       })
 
       it('should sync the subscription to the user', function () {
-        this.SubscriptionUpdater.syncSubscription.calledOnce.should.equal(true)
-        this.SubscriptionUpdater.syncSubscription.args[0][0].should.deep.equal(
+        this.SubscriptionUpdater.promises.syncSubscription.calledOnce.should.equal(
+          true
+        )
+        this.SubscriptionUpdater.promises.syncSubscription.args[0][0].should.deep.equal(
           this.activeRecurlySubscription
         )
-        this.SubscriptionUpdater.syncSubscription.args[0][1].should.deep.equal(
+        this.SubscriptionUpdater.promises.syncSubscription.args[0][1].should.deep.equal(
           this.user._id
         )
       })
@@ -526,52 +518,52 @@ describe('SubscriptionHandler', function () {
 
   describe('attemptPaypalInvoiceCollection', function () {
     describe('for credit card users', function () {
-      beforeEach(function (done) {
-        this.RecurlyWrapper.getBillingInfo.yields(null, {
+      beforeEach(async function () {
+        this.RecurlyWrapper.promises.getBillingInfo.resolves({
           paypal_billing_agreement_id: null,
         })
-        this.SubscriptionHandler.attemptPaypalInvoiceCollection(
-          this.activeRecurlySubscription.account.account_code,
-          done
+        await this.SubscriptionHandler.promises.attemptPaypalInvoiceCollection(
+          this.activeRecurlySubscription.account.account_code
         )
       })
 
       it('gets billing infos', function () {
         sinon.assert.calledWith(
-          this.RecurlyWrapper.getBillingInfo,
+          this.RecurlyWrapper.promises.getBillingInfo,
           this.activeRecurlySubscription.account.account_code
         )
       })
 
       it('skips user', function () {
-        sinon.assert.notCalled(this.RecurlyWrapper.getAccountPastDueInvoices)
+        sinon.assert.notCalled(
+          this.RecurlyWrapper.promises.getAccountPastDueInvoices
+        )
       })
     })
 
     describe('for paypal users', function () {
-      beforeEach(function (done) {
-        this.RecurlyWrapper.getBillingInfo.yields(null, {
+      beforeEach(async function () {
+        this.RecurlyWrapper.promises.getBillingInfo.resolves({
           paypal_billing_agreement_id: 'mock-billing-agreement',
         })
-        this.RecurlyWrapper.getAccountPastDueInvoices.yields(null, [
+        this.RecurlyWrapper.promises.getAccountPastDueInvoices.resolves([
           { invoice_number: 'mock-invoice-number' },
         ])
-        this.SubscriptionHandler.attemptPaypalInvoiceCollection(
-          this.activeRecurlySubscription.account.account_code,
-          done
+        await this.SubscriptionHandler.promises.attemptPaypalInvoiceCollection(
+          this.activeRecurlySubscription.account.account_code
         )
       })
 
       it('gets past due invoices', function () {
         sinon.assert.calledWith(
-          this.RecurlyWrapper.getAccountPastDueInvoices,
+          this.RecurlyWrapper.promises.getAccountPastDueInvoices,
           this.activeRecurlySubscription.account.account_code
         )
       })
 
       it('calls attemptInvoiceCollection', function () {
         sinon.assert.calledWith(
-          this.RecurlyWrapper.attemptInvoiceCollection,
+          this.RecurlyWrapper.promises.attemptInvoiceCollection,
           'mock-invoice-number'
         )
       })
@@ -580,47 +572,49 @@ describe('SubscriptionHandler', function () {
 
   describe('validateNoSubscriptionInRecurly', function () {
     describe('with a subscription in recurly', function () {
-      beforeEach(function () {
-        this.RecurlyWrapper.listAccountActiveSubscriptions.yields(null, [
+      beforeEach(async function () {
+        this.RecurlyWrapper.promises.listAccountActiveSubscriptions.resolves([
           this.subscription,
         ])
-        this.SubscriptionHandler.validateNoSubscriptionInRecurly(
-          this.user_id,
-          this.callback
-        )
+        this.isValid =
+          await this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
+            this.user_id
+          )
       })
 
       it('should call RecurlyWrapper.listAccountActiveSubscriptions with the user id', function () {
-        this.RecurlyWrapper.listAccountActiveSubscriptions
+        this.RecurlyWrapper.promises.listAccountActiveSubscriptions
           .calledWith(this.user_id)
           .should.equal(true)
       })
 
       it('should sync the subscription', function () {
-        this.SubscriptionUpdater.syncSubscription
+        this.SubscriptionUpdater.promises.syncSubscription
           .calledWith(this.subscription, this.user_id)
           .should.equal(true)
       })
 
-      it('should call the callback with valid == false', function () {
-        this.callback.calledWith(null, false).should.equal(true)
+      it('should return false', function () {
+        expect(this.isValid).to.equal(false)
       })
     })
 
     describe('with no subscription in recurly', function () {
-      beforeEach(function () {
-        this.SubscriptionHandler.validateNoSubscriptionInRecurly(
-          this.user_id,
-          this.callback
-        )
+      beforeEach(async function () {
+        this.isValid =
+          await this.SubscriptionHandler.promises.validateNoSubscriptionInRecurly(
+            this.user_id
+          )
       })
 
-      it('should not sync the subscription', function () {
-        this.SubscriptionUpdater.syncSubscription.called.should.equal(false)
+      it('should be rejected and not sync the subscription', function () {
+        this.SubscriptionUpdater.promises.syncSubscription.called.should.equal(
+          false
+        )
       })
 
-      it('should call the callback with valid == true', function () {
-        this.callback.calledWith(null, true).should.equal(true)
+      it('should return true', function () {
+        expect(this.isValid).to.equal(true)
       })
     })
   })