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

GitOrigin-RevId: a954f42e1159e4bcc8fd06f5f6df9a53c67f9f90
This commit is contained in:
Kristina
2025-05-12 11:28:43 +02:00
committed by Copybot
parent 0d70223a48
commit 70c26b6ed2
10 changed files with 129 additions and 125 deletions
@@ -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
@@ -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
)
@@ -1,10 +0,0 @@
export default function isMonthlyCollaboratorPlan(
planCode: string,
isGroupPlan?: boolean
) {
return (
planCode.indexOf('collaborator') !== -1 &&
planCode.indexOf('ann') === -1 &&
!isGroupPlan
)
}
@@ -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)
)
}
@@ -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', {
@@ -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,
},
}
@@ -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
})
})
@@ -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
})
})
@@ -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({
@@ -46,6 +46,7 @@ type PaymentProviderRecord = {
remainingPauseCycles?: Nullable<number>
isEligibleForPause: boolean
isEligibleForGroupPlan: boolean
isEligibleForDowngradeUpsell: boolean
}
export type GroupPolicy = {