Merge pull request #25082 from overleaf/kh-prevent-pausing-and-group-plans

[web] prevent pausing or upgrading to group for Stripe subscriptions

GitOrigin-RevId: 4d194339282d8bc165ffa1b89e8e1cf298c2d343
This commit is contained in:
Kristina
2025-04-29 08:05:09 +00:00
committed by Copybot
parent 71094cb283
commit 23c1a0ba4d
8 changed files with 176 additions and 23 deletions
@@ -242,6 +242,9 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
}
})
const totalLicenses = (plan.membersLimit || 0) + additionalLicenses
const isInTrial =
paymentRecord.subscription.trialPeriodEnd &&
paymentRecord.subscription.trialPeriodEnd.getTime() > Date.now()
personalSubscription.payment = {
taxRate,
billingDetailsLink:
@@ -269,6 +272,15 @@ async function buildUsersSubscriptionViewModel(user, locale = 'en') {
hasPastDueInvoice: paymentRecord.account.hasPastDueInvoice,
pausedAt: paymentRecord.subscription.pausePeriodStart,
remainingPauseCycles: paymentRecord.subscription.remainingPauseCycles,
isEligibleForPause:
paymentRecord.subscription.service === 'recurly' &&
!personalSubscription.pendingPlan &&
!personalSubscription.groupPlan &&
!isInTrial &&
!paymentRecord.subscription.planCode.includes('ann') &&
!paymentRecord.subscription.addOns?.length > 0,
isEligibleForGroupPlan:
paymentRecord.subscription.service === 'recurly' && !isInTrial,
}
if (paymentRecord.subscription.pendingChange) {
const pendingPlanCode =
@@ -1560,6 +1560,7 @@
"sorry_there_was_an_issue_adding_x_users_to_your_subscription": "",
"sorry_there_was_an_issue_upgrading_your_subscription": "",
"sorry_you_can_only_change_to_group_from_trial_via_support": "",
"sorry_you_can_only_change_to_group_via_support": "",
"sorry_your_table_cant_be_displayed_at_the_moment": "",
"sort_by": "",
"sort_by_x": "",
@@ -24,12 +24,7 @@ export function CancelSubscriptionButton() {
(subscription.payment.state === 'active' &&
subscription.payment.remainingPauseCycles &&
subscription.payment.remainingPauseCycles > 0)
const planIsEligibleForPause =
!subscription.pendingPlan &&
!subscription.groupPlan &&
!isInTrial &&
!subscription.planCode.includes('ann') &&
!subscription.addOns?.length
const planIsEligibleForPause = subscription.payment.isEligibleForPause
const enablePause =
useFeatureFlag('pause-subscription') &&
!hasPendingOrActivePause &&
@@ -15,6 +15,7 @@ export function ChangeToGroupPlan() {
personalSubscription && 'payment' in personalSubscription
? (personalSubscription as PaidSubscription)
: null
const isInTrial = isInFreeTrial(subscription?.payment?.trialEndsAt)
const handleClick = () => {
handleOpenModal('change-to-group')
@@ -25,13 +26,15 @@ export function ChangeToGroupPlan() {
<h2 style={{ marginTop: 0 }}>{t('looking_multiple_licenses')}</h2>
<p style={{ margin: 0 }}>{t('reduce_costs_group_licenses')}</p>
<br />
{isInFreeTrial(subscription?.payment?.trialEndsAt) ? (
{!subscription?.payment?.isEligibleForGroupPlan ? (
<>
<OLTooltip
id="disabled-change-to-group-plan"
description={t(
'sorry_you_can_only_change_to_group_from_trial_via_support'
)}
description={
isInTrial
? t('sorry_you_can_only_change_to_group_from_trial_via_support')
: t('sorry_you_can_only_change_to_group_via_support')
}
overlayProps={{ placement: 'top' }}
>
<div>
+1
View File
@@ -2042,6 +2042,7 @@
"sorry_there_was_an_issue_upgrading_your_subscription": "Sorry, there was an issue upgrading your subscription. Please <0>contact our Support team</0> for help.",
"sorry_this_account_has_been_suspended": "Sorry, this account has been suspended.",
"sorry_you_can_only_change_to_group_from_trial_via_support": "Sorry, you can only change to a group plan during a free trial by contacting support.",
"sorry_you_can_only_change_to_group_via_support": "Sorry, you can only change to a group plan by contacting support.",
"sorry_your_table_cant_be_displayed_at_the_moment": "Sorry, your table cant be displayed at the moment.",
"sorry_your_token_expired": "Sorry, your token expired",
"sort_by": "Sort by",
@@ -52,6 +52,8 @@ export const annualActiveSubscription: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
},
}
@@ -92,6 +94,8 @@ export const annualActiveSubscriptionEuro: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -131,6 +135,8 @@ export const annualActiveSubscriptionPro: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -171,6 +177,8 @@ export const pastDueExpiredSubscription: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -211,6 +219,8 @@ export const canceledSubscription: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -251,6 +261,8 @@ export const pendingSubscriptionChange: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
},
pendingPlan: {
planCode: 'professional-annual',
@@ -302,6 +314,8 @@ export const groupActiveSubscription: GroupSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
},
}
@@ -349,6 +363,8 @@ export const groupActiveSubscriptionWithPendingLicenseChange: GroupSubscription
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
},
pendingPlan: {
planCode: 'group_collaborator_10_enterprise',
@@ -399,6 +415,8 @@ export const trialSubscription: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
},
}
@@ -460,6 +478,8 @@ export const trialCollaboratorSubscription: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -499,5 +519,7 @@ export const monthlyActiveCollaborator: PaidSubscription = {
planOnlyDisplayPrice: '',
addOns: [],
addOnDisplayPricesWithoutAdditionalLicense: {},
isEligibleForGroupPlan: true,
isEligibleForPause: true,
},
}
@@ -507,12 +507,24 @@ describe('SubscriptionViewModelBuilder', function () {
})
describe('buildUsersSubscriptionViewModel', function () {
describe('with a recurly subscription', function () {
beforeEach(function () {
this.SubscriptionLocator.getUsersSubscription.yields(
null,
this.individualSubscription
)
this.Modules.hooks.fire
.withArgs('getPaymentFromRecord', this.individualSubscription)
.yields(null, [
{
subscription: this.paymentRecord,
account: new PaymentProviderAccount({}),
coupons: [],
},
])
})
describe('with a paid subscription', function () {
it('adds payment data to the personal subscription', async function () {
this.SubscriptionLocator.getUsersSubscription.yields(
null,
this.individualSubscription
)
this.Modules.hooks.fire
.withArgs('getPaymentFromRecord', this.individualSubscription)
.yields(null, [
@@ -561,14 +573,123 @@ describe('SubscriptionViewModelBuilder', function () {
addOnDisplayPricesWithoutAdditionalLicense: {
'addon-code': '€2.20',
},
isEligibleForGroupPlan: true,
isEligibleForPause: false,
})
})
describe('isEligibleForGroupPlan', function () {
it('is false for Stripe subscriptions', async function () {
this.paymentRecord.service = 'stripe'
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(
result.personalSubscription.payment.isEligibleForGroupPlan
)
})
it('is false when in trial', async function () {
const msIn24Hours = 24 * 60 * 60 * 1000
const tomorrow = new Date(Date.now() + msIn24Hours)
this.paymentRecord.trialPeriodEnd = tomorrow
this.paymentRecord.service = 'recurly'
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(
result.personalSubscription.payment.isEligibleForGroupPlan
)
})
it('is true when not in trial and for a Recurly subscription', async function () {
this.paymentRecord.service = 'recurly'
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isTrue(
result.personalSubscription.payment.isEligibleForGroupPlan
)
})
})
describe('isEligibleForPause', function () {
it('is false for Stripe subscriptions', async function () {
this.paymentRecord.service = 'stripe'
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for subscriptions with pending plan', async function () {
this.paymentRecord.service = 'recurly'
this.individualSubscription.pendingPlan = {} // anything
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for a group subscription', async function () {
this.paymentRecord.service = 'recurly'
this.individualSubscription.groupPlan = true
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false when in trial', async function () {
this.paymentRecord.service = 'recurly'
const msIn24Hours = 24 * 60 * 60 * 1000
const tomorrow = new Date(Date.now() + msIn24Hours)
this.paymentRecord.trialPeriodEnd = tomorrow
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for annual subscriptions', async function () {
this.paymentRecord.service = 'recurly'
this.paymentRecord.planCode = 'collaborator-annual'
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is false for subscriptions with add-ons', async function () {
this.paymentRecord.service = 'recurly'
this.paymentRecord.addOns = [{}] // anything
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isFalse(result.personalSubscription.payment.isEligibleForPause)
})
it('is true when conditions are met', async function () {
this.paymentRecord.service = 'recurly'
this.paymentRecord.addOns = []
const result =
await this.SubscriptionViewModelBuilder.promises.buildUsersSubscriptionViewModel(
this.user
)
assert.isTrue(result.personalSubscription.payment.isEligibleForPause)
})
})
it('includes pending changes', async function () {
this.SubscriptionLocator.getUsersSubscription.yields(
null,
this.individualSubscription
)
this.paymentRecord.pendingChange =
new PaymentProviderSubscriptionChange({
subscription: this.paymentRecord,
@@ -628,10 +749,6 @@ describe('SubscriptionViewModelBuilder', function () {
it('does not add a billing details link for a Stripe subscription', async function () {
this.paymentRecord.service = 'stripe'
this.SubscriptionLocator.getUsersSubscription.yields(
null,
this.individualSubscription
)
this.Modules.hooks.fire
.withArgs('getPaymentFromRecord', this.individualSubscription)
.yields(null, [
@@ -44,6 +44,8 @@ type PaymentProviderRecord = {
pendingTotalLicenses?: number
pausedAt?: Nullable<string>
remainingPauseCycles?: Nullable<number>
isEligibleForPause: boolean
isEligibleForGroupPlan: boolean
}
export type GroupPolicy = {