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:
@@ -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": "",
|
||||
|
||||
+1
-6
@@ -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 &&
|
||||
|
||||
+7
-4
@@ -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>
|
||||
|
||||
@@ -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 can’t 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 = {
|
||||
|
||||
Reference in New Issue
Block a user