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

[web] prevent downgrade to personal upsell for stripe subscriptions (#25392)

GitOrigin-RevId: a954f42e1159e4bcc8fd06f5f6df9a53c67f9f90
Kristina 1 год назад
Родитель
Сommit
70c26b6ed2

+ 12 - 0
services/web/app/src/Features/Subscription/SubscriptionViewModelBuilder.js

@@ -282,6 +282,18 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
       isEligibleForGroupPlan:
         paymentRecord.subscription.service === 'recurly' && !isInTrial,
     }
+
+    const isMonthlyCollaboratorPlan =
+      personalSubscription.planCode.includes('collaborator') &&
+      !personalSubscription.planCode.includes('ann') &&
+      !personalSubscription.plan.groupPlan
+    personalSubscription.payment.isEligibleForDowngradeUpsell =
+      !personalSubscription.payment.pausedAt &&
+      !personalSubscription.payment.remainingPauseCycles &&
+      isMonthlyCollaboratorPlan &&
+      !isInTrial &&
+      paymentRecord.subscription.service === 'recurly'
+
     if (paymentRecord.subscription.pendingChange) {
       const pendingPlanCode =
         paymentRecord.subscription.pendingChange.nextPlanCode

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

@@ -8,7 +8,6 @@ import {
   cancelSubscriptionUrl,
   redirectAfterCancelSubscriptionUrl,
 } from '../../../../../data/subscription-url'
-import showDowngradeOption from '../../../../../util/show-downgrade-option'
 import GenericErrorAlert from '../../../generic-error-alert'
 import DowngradePlanButton from './downgrade-plan-button'
 import ExtendTrialButton from './extend-trial-button'
@@ -157,13 +156,8 @@ export function CancelSubscription() {
 
   if (!personalSubscription || !('payment' in personalSubscription)) return null
 
-  const showDowngrade = showDowngradeOption(
-    personalSubscription.plan.planCode,
-    personalSubscription.plan.groupPlan,
-    personalSubscription.payment.trialEndsAt,
-    personalSubscription.payment.pausedAt,
-    personalSubscription.payment.remainingPauseCycles
-  )
+  const showDowngrade =
+    personalSubscription.payment.isEligibleForDowngradeUpsell
   const planToDowngradeTo = plans.find(
     plan => plan.planCode === planCodeToDowngradeTo
   )

+ 0 - 10
services/web/frontend/js/features/subscription/util/is-monthly-collaborator-plan.ts

@@ -1,10 +0,0 @@
-export default function isMonthlyCollaboratorPlan(
-  planCode: string,
-  isGroupPlan?: boolean
-) {
-  return (
-    planCode.indexOf('collaborator') !== -1 &&
-    planCode.indexOf('ann') === -1 &&
-    !isGroupPlan
-  )
-}

+ 0 - 18
services/web/frontend/js/features/subscription/util/show-downgrade-option.ts

@@ -1,18 +0,0 @@
-import { Nullable } from '../../../../../types/utils'
-import isInFreeTrial from './is-in-free-trial'
-import isMonthlyCollaboratorPlan from './is-monthly-collaborator-plan'
-
-export default function showDowngradeOption(
-  planCode: string,
-  isGroupPlan?: boolean,
-  trialEndsAt?: string | null,
-  pausedAt?: Nullable<string>,
-  remainingPauseCycles?: Nullable<number>
-) {
-  return (
-    !pausedAt &&
-    !remainingPauseCycles &&
-    isMonthlyCollaboratorPlan(planCode, isGroupPlan) &&
-    !isInFreeTrial(trialEndsAt)
-  )
-}

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

@@ -470,10 +470,10 @@ describe('<ActiveSubscription />', function () {
         })
       })
 
-      it('does not show option to downgrade when not a collaborator plan', function () {
-        const trialPlan = cloneDeep(monthlyActiveCollaborator)
-        trialPlan.plan.planCode = 'anotherplan'
-        renderActiveSubscription(trialPlan)
+      it('does not show option to downgrade when plan is not eligible for downgrades', function () {
+        const ineligiblePlan = cloneDeep(monthlyActiveCollaborator)
+        ineligiblePlan.payment.isEligibleForDowngradeUpsell = false
+        renderActiveSubscription(ineligiblePlan)
         showConfirmCancelUI()
         expect(
           screen.queryByRole('button', {

+ 11 - 0
services/web/test/frontend/features/subscription/fixtures/subscriptions.ts

@@ -54,6 +54,7 @@ export const annualActiveSubscription: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: false,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -96,6 +97,7 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -137,6 +139,7 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -179,6 +182,7 @@ export const pastDueExpiredSubscription: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -221,6 +225,7 @@ export const canceledSubscription: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -263,6 +268,7 @@ export const pendingSubscriptionChange: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: false,
+    isEligibleForDowngradeUpsell: false,
   },
   pendingPlan: {
     planCode: 'professional-annual',
@@ -316,6 +322,7 @@ export const groupActiveSubscription: GroupSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: false,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -365,6 +372,7 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
       addOnDisplayPricesWithoutAdditionalLicense: {},
       isEligibleForGroupPlan: true,
       isEligibleForPause: false,
+      isEligibleForDowngradeUpsell: false,
     },
     pendingPlan: {
       planCode: 'group_collaborator_10_enterprise',
@@ -417,6 +425,7 @@ export const trialSubscription: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: false,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -480,6 +489,7 @@ export const trialCollaboratorSubscription: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: false,
   },
 }
 
@@ -521,5 +531,6 @@ export const monthlyActiveCollaborator: PaidSubscription = {
     addOnDisplayPricesWithoutAdditionalLicense: {},
     isEligibleForGroupPlan: true,
     isEligibleForPause: true,
+    isEligibleForDowngradeUpsell: true,
   },
 }

+ 0 - 17
services/web/test/frontend/features/subscription/util/is-monthly-collaborator-plan.test.ts

@@ -1,17 +0,0 @@
-import { expect } from 'chai'
-import isMonthlyCollaboratorPlan from '../../../../../frontend/js/features/subscription/util/is-monthly-collaborator-plan'
-
-describe('isMonthlyCollaboratorPlan', function () {
-  it('returns false when a plan code without "collaborator" ', function () {
-    expect(isMonthlyCollaboratorPlan('test', false)).to.be.false
-  })
-  it('returns false when on a plan with "collaborator" and "ann"', function () {
-    expect(isMonthlyCollaboratorPlan('collaborator-annual', false)).to.be.false
-  })
-  it('returns false when on a plan with "collaborator" and without "ann" but is a group plan', function () {
-    expect(isMonthlyCollaboratorPlan('collaborator', true)).to.be.false
-  })
-  it('returns true when on a plan with non-group "collaborator" monthly plan', function () {
-    expect(isMonthlyCollaboratorPlan('collaborator', false)).to.be.true
-  })
-})

+ 0 - 68
services/web/test/frontend/features/subscription/util/show-downgrade-option.test.ts

@@ -1,68 +0,0 @@
-import { expect } from 'chai'
-import showDowngradeOption from '../../../../../frontend/js/features/subscription/util/show-downgrade-option'
-import dateformat from 'dateformat'
-
-describe('showDowngradeOption', function () {
-  const today = new Date()
-  const sevenDaysFromToday = new Date().setDate(today.getDate() + 7)
-  const sevenDaysFromTodayFormatted = dateformat(
-    sevenDaysFromToday,
-    'dS mmmm yyyy'
-  )
-
-  it('returns false when no trial end date', function () {
-    expect(showDowngradeOption('collab')).to.be.false
-  })
-  it('returns false when a plan code without "collaborator" ', function () {
-    expect(showDowngradeOption('test', false, sevenDaysFromTodayFormatted)).to
-      .be.false
-  })
-  it('returns false when on a plan with trial date in future but has "collaborator" and "ann" in plan code', function () {
-    expect(
-      showDowngradeOption(
-        'collaborator-annual',
-        false,
-        sevenDaysFromTodayFormatted
-      )
-    ).to.be.false
-  })
-  it('returns false when on a plan with trial date in future and plan code has "collaborator" and no "ann" but is a group plan', function () {
-    expect(
-      showDowngradeOption('collaborator', true, sevenDaysFromTodayFormatted)
-    ).to.be.false
-  })
-  it('returns false when on a plan with "collaborator" and without "ann" and trial date in future', function () {
-    expect(
-      showDowngradeOption('collaborator', false, sevenDaysFromTodayFormatted)
-    ).to.be.false
-  })
-  it('returns true when on a plan with "collaborator" and without "ann" and no trial date', function () {
-    expect(showDowngradeOption('collaborator', false)).to.be.true
-  })
-  it('returns true when on a plan with "collaborator" and without "ann" and trial date is in the past', function () {
-    expect(
-      showDowngradeOption('collaborator', false, '2000-02-16T17:59:07.000Z')
-    ).to.be.true
-  })
-  it('returns false when on a monthly collaborator plan with a pending pause', function () {
-    expect(
-      showDowngradeOption(
-        'collaborator',
-        false,
-        null,
-        '2030-01-01T12:00:00.000Z'
-      )
-    ).to.be.false
-  })
-  it('returns false when on a monthly collaborator plan with an active pause', function () {
-    expect(
-      showDowngradeOption(
-        'collaborator',
-        false,
-        null,
-        '2030-01-01T12:00:00.000Z',
-        5
-      )
-    ).to.be.false
-  })
-})

+ 99 - 0
services/web/test/unit/src/Subscription/SubscriptionViewModelBuilderTests.js

@@ -25,6 +25,11 @@ describe('SubscriptionViewModelBuilder', function () {
       planCode: this.planCode,
       features: this.planFeatures,
     }
+    this.annualPlanCode = 'collaborator_annual'
+    this.annualPlan = {
+      planCode: this.annualPlanCode,
+      features: this.planFeatures,
+    }
     this.individualSubscription = {
       planCode: this.planCode,
       plan: this.plan,
@@ -74,6 +79,7 @@ describe('SubscriptionViewModelBuilder', function () {
       features: this.groupPlanFeatures,
       membersLimit: 4,
       membersLimitAddOn: 'additional-license',
+      groupPlan: true,
     }
     this.groupSubscription = {
       planCode: this.groupPlanCode,
@@ -166,6 +172,8 @@ describe('SubscriptionViewModelBuilder', function () {
     this.PlansLocator.findLocalPlanInSettings
       .withArgs(this.planCode)
       .returns(this.plan)
+      .withArgs(this.annualPlanCode)
+      .returns(this.annualPlan)
       .withArgs(this.groupPlanCode)
       .returns(this.groupPlan)
       .withArgs(this.commonsPlanCode)
@@ -575,6 +583,7 @@ describe('SubscriptionViewModelBuilder', function () {
           },
           isEligibleForGroupPlan: true,
           isEligibleForPause: false,
+          isEligibleForDowngradeUpsell: true,
         })
       })
 
@@ -689,6 +698,96 @@ describe('SubscriptionViewModelBuilder', function () {
         })
       })
 
+      describe('isEligibleForDowngradeUpsell', function () {
+        it('is true for eligible individual subscriptions', async function () {
+          this.paymentRecord.pausePeriodStart = null
+          this.paymentRecord.remainingPauseCycles = null
+          this.paymentRecord.trialPeriodEnd = null
+          this.paymentRecord.service = 'recurly'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isTrue(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+
+        it('is false for group plans', async function () {
+          this.individualSubscription.planCode = this.groupPlanCode
+          this.paymentRecord.pausePeriodStart = null
+          this.paymentRecord.remainingPauseCycles = null
+          this.paymentRecord.trialPeriodEnd = null
+          this.paymentRecord.service = 'recurly'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+
+        it('is false for annual individual plans', async function () {
+          this.individualSubscription.planCode = this.annualPlanCode
+          this.paymentRecord.pausePeriodStart = null
+          this.paymentRecord.remainingPauseCycles = null
+          this.paymentRecord.trialPeriodEnd = null
+          this.paymentRecord.service = 'recurly'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+
+        it('is false for paused plans', async function () {
+          this.paymentRecord.pausePeriodStart = new Date()
+          this.paymentRecord.remainingPauseCycles = 1
+          this.paymentRecord.trialPeriodEnd = null
+          this.paymentRecord.service = 'recurly'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+
+        it('is false for plans in free trial period', async function () {
+          this.paymentRecord.pausePeriodStart = null
+          this.paymentRecord.remainingPauseCycles = null
+          this.paymentRecord.trialPeriodEnd = new Date(
+            Date.now() + 24 * 60 * 60 * 1000 // tomorrow
+          )
+          this.paymentRecord.service = 'recurly'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+
+        it('is false for Stripe subscriptions', async function () {
+          this.paymentRecord.pausePeriodStart = null
+          this.paymentRecord.remainingPauseCycles = null
+          this.paymentRecord.trialPeriodEnd = null
+          this.paymentRecord.service = 'stripe'
+          const result =
+            await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
+              this.user
+            )
+          assert.isFalse(
+            result.personalSubscription.payment.isEligibleForDowngradeUpsell
+          )
+        })
+      })
+
       it('includes pending changes', async function () {
         this.paymentRecord.pendingChange =
           new PaymentProviderSubscriptionChange({

+ 1 - 0
services/web/types/subscription/dashboard/subscription.ts

@@ -46,6 +46,7 @@ type PaymentProviderRecord = {
   remainingPauseCycles?: Nullable<number>
   isEligibleForPause: boolean
   isEligibleForGroupPlan: boolean
+  isEligibleForDowngradeUpsell: boolean
 }
 
 export type GroupPolicy = {