瀏覽代碼

Merge pull request #26494 from overleaf/kh-fix-trial-transitions

[web] fix upgrades/downgrades while on trial

GitOrigin-RevId: 4076befc5dbbee32b0cf2a4ff99db96a0cf3ad8b
Kristina 1 年之前
父節點
當前提交
b15758da97

+ 5 - 2
services/web/app/src/Features/Subscription/PaymentProviderEntities.js

@@ -134,9 +134,11 @@ class PaymentProviderSubscription {
     if (newPlan == null) {
       throw new OError('Unable to find plan in settings', { planCode })
     }
+    const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd)
     const shouldChangeAtTermEnd = SubscriptionHelper.shouldPlanChangeAtTermEnd(
       currentPlan,
-      newPlan
+      newPlan,
+      isInTrial
     )
 
     const changeRequest = new PaymentProviderSubscriptionChangeRequest({
@@ -250,9 +252,10 @@ class PaymentProviderSubscription {
     const addOnUpdates = this.addOns
       .filter(addOn => addOn.code !== code)
       .map(addOn => addOn.toAddOnUpdate())
+    const isInTrial = SubscriptionHelper.isInTrial(this.trialPeriodEnd)
     return new PaymentProviderSubscriptionChangeRequest({
       subscription: this,
-      timeframe: 'term_end',
+      timeframe: isInTrial ? 'now' : 'term_end',
       addOnUpdates,
     })
   }

+ 5 - 1
services/web/app/src/Features/Subscription/SubscriptionController.js

@@ -88,9 +88,13 @@ async function userSubscriptionPage(req, res) {
     await Modules.promises.hooks.fire('userCanExtendTrial', user)
   )?.[0]
   const fromPlansPage = req.query.hasSubscription
+  const isInTrial = SubscriptionHelper.isInTrial(
+    personalSubscription?.payment?.trialEndsAt
+  )
   const plansData =
     SubscriptionViewModelBuilder.buildPlansListForSubscriptionDash(
-      personalSubscription?.plan
+      personalSubscription?.plan,
+      isInTrial
     )
 
   AnalyticsManager.recordEventForSession(req.session, 'subscription-page-view')

+ 15 - 1
services/web/app/src/Features/Subscription/SubscriptionHelper.js

@@ -6,7 +6,12 @@ const { isStandaloneAiAddOnPlanCode } = require('./PaymentProviderEntities')
  * If the user changes to a less expensive plan, we shouldn't apply the change immediately.
  * This is to avoid unintended/artifical credits on users Recurly accounts.
  */
-function shouldPlanChangeAtTermEnd(oldPlan, newPlan) {
+function shouldPlanChangeAtTermEnd(oldPlan, newPlan, isInTrial) {
+  if (isInTrial) {
+    // we should always upgrade or downgrade immediately if actively in trial
+    return false
+  }
+
   if (
     oldPlan.annual === newPlan.annual &&
     isStandaloneAiAddOnPlanCode(oldPlan.planCode) &&
@@ -148,6 +153,14 @@ function getSubscriptionTrialEndsAt(subscription) {
   return subscription?.paymentProvider?.trialEndsAt
 }
 
+function isInTrial(trialEndsAt) {
+  if (!trialEndsAt) {
+    return false
+  }
+
+  return trialEndsAt.getTime() > Date.now()
+}
+
 module.exports = {
   shouldPlanChangeAtTermEnd,
   generateInitialLocalizedGroupPrice,
@@ -157,4 +170,5 @@ module.exports = {
   getPaidSubscriptionState,
   getSubscriptionTrialStartedAt,
   getSubscriptionTrialEndsAt,
+  isInTrial,
 }

+ 8 - 4
services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js

@@ -470,7 +470,7 @@ async function getUsersSubscriptionDetails(user) {
   return { bestSubscription, individualSubscription, memberGroupSubscriptions }
 }
 
-function buildPlansList(currentPlan) {
+function buildPlansList(currentPlan, isInTrial) {
   const { plans } = Settings
 
   const allPlans = {}
@@ -484,7 +484,11 @@ function buildPlansList(currentPlan) {
     result.planCodesChangingAtTermEnd = _.map(
       _.filter(plans, plan => {
         if (!plan.hideFromUsers) {
-          return SubscriptionHelper.shouldPlanChangeAtTermEnd(currentPlan, plan)
+          return SubscriptionHelper.shouldPlanChangeAtTermEnd(
+            currentPlan,
+            plan,
+            isInTrial
+          )
         }
       }),
       'planCode'
@@ -569,8 +573,8 @@ function buildGroupSubscriptionForView(groupSubscription) {
   }
 }
 
-function buildPlansListForSubscriptionDash(currentPlan) {
-  const allPlansData = buildPlansList(currentPlan)
+function buildPlansListForSubscriptionDash(currentPlan, isInTrial) {
+  const allPlansData = buildPlansList(currentPlan, isInTrial)
   const plans = []
   // only list individual and visible plans for "change plans" UI
   if (allPlansData.studentAccounts) {

+ 33 - 0
services/web/test/unit/src/Subscription/PaymentProviderEntitiesTest.js

@@ -104,6 +104,23 @@ describe('PaymentProviderEntities', function () {
           )
         })
 
+        it('returns a change request for downgrades while on trial', function () {
+          const fiveDaysFromNow = new Date()
+          fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
+          this.subscription.trialPeriodEnd = fiveDaysFromNow
+          const { PaymentProviderSubscriptionChangeRequest } =
+            this.PaymentProviderEntities
+          const changeRequest =
+            this.subscription.getRequestForPlanChange('cheap-plan')
+          expect(changeRequest).to.deep.equal(
+            new PaymentProviderSubscriptionChangeRequest({
+              subscription: this.subscription,
+              timeframe: 'now',
+              planCode: 'cheap-plan',
+            })
+          )
+        })
+
         it('preserves the AI add-on on upgrades', function () {
           const { PaymentProviderSubscriptionChangeRequest } =
             this.PaymentProviderEntities
@@ -282,6 +299,22 @@ describe('PaymentProviderEntities', function () {
           )
         })
 
+        it('returns a change request when in trial', function () {
+          const fiveDaysFromNow = new Date()
+          fiveDaysFromNow.setDate(fiveDaysFromNow.getDate() + 5)
+          this.subscription.trialPeriodEnd = fiveDaysFromNow
+          const changeRequest = this.subscription.getRequestForAddOnRemoval(
+            this.addOn.code
+          )
+          expect(changeRequest).to.deep.equal(
+            new PaymentProviderSubscriptionChangeRequest({
+              subscription: this.subscription,
+              timeframe: 'now',
+              addOnUpdates: [],
+            })
+          )
+        })
+
         it("throws an AddOnNotPresentError if the subscription doesn't have the add-on", function () {
           expect(() =>
             this.subscription.getRequestForAddOnRemoval('another-add-on')

+ 53 - 5
services/web/test/unit/src/Subscription/SubscriptionHelperTests.js

@@ -102,38 +102,65 @@ describe('SubscriptionHelper', function () {
   })
 
   describe('shouldPlanChangeAtTermEnd', function () {
+    it('should return false if isInTrial is true', function () {
+      const isInTrial = true
+      const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
+        plans.expensive,
+        plans.cheaper,
+        isInTrial
+      )
+      expect(changeAtTermEnd).to.be.false
+    })
+
     it('should return true if the new plan is less expensive', function () {
+      const isInTrial = false
       const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
         plans.expensive,
-        plans.cheaper
+        plans.cheaper,
+        isInTrial
       )
       expect(changeAtTermEnd).to.be.true
     })
+
     it('should return false if the new plan is more exepensive', function () {
+      const isInTrial = false
       const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
         plans.cheaper,
-        plans.expensive
+        plans.expensive,
+        isInTrial
       )
       expect(changeAtTermEnd).to.be.false
     })
+
     it('should return false if the new plan is the same price', function () {
+      const isInTrial = false
+
       const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
         plans.cheaper,
-        plans.alsoCheap
+        plans.alsoCheap,
+        isInTrial
       )
       expect(changeAtTermEnd).to.be.false
     })
+
     it('should return false if the change is from an individual plan to a more expensive group plan', function () {
+      const isInTrial = false
+
       const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
         plans.expensive,
-        plans.expensiveGroup
+        plans.expensiveGroup,
+        isInTrial
       )
       expect(changeAtTermEnd).to.be.false
     })
+
     it('should return true if the change is from an individual plan to a cheaper group plan', function () {
+      const isInTrial = false
+
       const changeAtTermEnd = this.SubscriptionHelper.shouldPlanChangeAtTermEnd(
         plans.expensive,
-        plans.cheapGroup
+        plans.cheapGroup,
+        isInTrial
       )
       expect(changeAtTermEnd).to.be.true
     })
@@ -471,4 +498,25 @@ describe('SubscriptionHelper', function () {
       expect(result).to.be.undefined
     })
   })
+
+  describe('isInTrial', function () {
+    it('should return false if trialEndsAt is null', function () {
+      const result = this.SubscriptionHelper.isInTrial(null)
+      expect(result).to.be.false
+    })
+
+    it('should return false if trialEndsAt is before now', function () {
+      const tenDaysAgo = new Date()
+      tenDaysAgo.setDate(tenDaysAgo.getDate() - 10)
+      const result = this.SubscriptionHelper.isInTrial(tenDaysAgo)
+      expect(result).to.be.false
+    })
+
+    it('should return true if trialEndsAt is after now', function () {
+      const tenDaysFromNow = new Date()
+      tenDaysFromNow.setDate(tenDaysFromNow.getDate() + 10)
+      const result = this.SubscriptionHelper.isInTrial(tenDaysFromNow)
+      expect(result).to.be.true
+    })
+  })
 })