mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-02 21:59:00 +02:00
[web] prevent downgrade to personal upsell for stripe subscriptions (#25392)
GitOrigin-RevId: a954f42e1159e4bcc8fd06f5f6df9a53c67f9f90
This commit is contained in:
@@ -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
@@ -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)
|
||||
)
|
||||
}
|
||||
+4
-4
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
-17
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user