mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-06-08 16:50:44 +02:00
Merge pull request #23563 from overleaf/ii-flexible-group-licensing-errors
[web] Flexible licensing error handling GitOrigin-RevId: 9fd4992a81e67b0684d3e286492e0037dd56e2ea
This commit is contained in:
@@ -20,6 +20,8 @@ class ManuallyCollectedError extends OError {}
|
||||
|
||||
class PendingChangeError extends OError {}
|
||||
|
||||
class InactiveError extends OError {}
|
||||
|
||||
module.exports = {
|
||||
RecurlyTransactionError,
|
||||
DuplicateAddOnError,
|
||||
@@ -27,4 +29,5 @@ module.exports = {
|
||||
MissingBillingInfoError,
|
||||
ManuallyCollectedError,
|
||||
PendingChangeError,
|
||||
InactiveError,
|
||||
}
|
||||
|
||||
@@ -13,7 +13,12 @@ import ErrorController from '../Errors/ErrorController.js'
|
||||
import UserGetter from '../User/UserGetter.js'
|
||||
import { Subscription } from '../../models/Subscription.js'
|
||||
import { isProfessionalGroupPlan } from './PlansHelper.mjs'
|
||||
import { MissingBillingInfoError, ManuallyCollectedError } from './Errors.js'
|
||||
import {
|
||||
MissingBillingInfoError,
|
||||
ManuallyCollectedError,
|
||||
PendingChangeError,
|
||||
InactiveError,
|
||||
} from './Errors.js'
|
||||
import RecurlyClient from './RecurlyClient.js'
|
||||
|
||||
/**
|
||||
@@ -150,11 +155,6 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
isProfessional: isProfessionalGroupPlan(subscription),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.err(
|
||||
{ error },
|
||||
'error while getting users group subscription details'
|
||||
)
|
||||
|
||||
if (error instanceof MissingBillingInfoError) {
|
||||
return res.redirect(
|
||||
'/user/subscription/group/missing-billing-information'
|
||||
@@ -167,6 +167,15 @@ async function addSeatsToGroupSubscription(req, res) {
|
||||
)
|
||||
}
|
||||
|
||||
if (error instanceof PendingChangeError || error instanceof InactiveError) {
|
||||
return res.redirect('/user/subscription')
|
||||
}
|
||||
|
||||
logger.err(
|
||||
{ error },
|
||||
'error while getting users group subscription details'
|
||||
)
|
||||
|
||||
return res.redirect('/user/subscription')
|
||||
}
|
||||
}
|
||||
@@ -187,11 +196,21 @@ async function previewAddSeatsSubscriptionChange(req, res) {
|
||||
|
||||
res.json(preview)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof MissingBillingInfoError ||
|
||||
error instanceof ManuallyCollectedError ||
|
||||
error instanceof PendingChangeError ||
|
||||
error instanceof InactiveError
|
||||
) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
logger.err(
|
||||
{ error },
|
||||
'error trying to preview "add seats" subscription change'
|
||||
)
|
||||
return res.status(400).end()
|
||||
|
||||
return res.status(500).end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,11 +230,21 @@ async function createAddSeatsSubscriptionChange(req, res) {
|
||||
|
||||
res.json(create)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof MissingBillingInfoError ||
|
||||
error instanceof ManuallyCollectedError ||
|
||||
error instanceof PendingChangeError ||
|
||||
error instanceof InactiveError
|
||||
) {
|
||||
return res.status(422).end()
|
||||
}
|
||||
|
||||
logger.err(
|
||||
{ error },
|
||||
'error trying to create "add seats" subscription change'
|
||||
)
|
||||
return res.status(400).end()
|
||||
|
||||
return res.status(500).end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,8 +301,6 @@ async function subscriptionUpgradePage(req, res) {
|
||||
groupName: olSubscription.teamName,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.err({ error }, 'error loading upgrade subscription page')
|
||||
|
||||
if (error instanceof MissingBillingInfoError) {
|
||||
return res.redirect(
|
||||
'/user/subscription/group/missing-billing-information'
|
||||
@@ -286,6 +313,12 @@ async function subscriptionUpgradePage(req, res) {
|
||||
)
|
||||
}
|
||||
|
||||
if (error instanceof PendingChangeError || error instanceof InactiveError) {
|
||||
return res.redirect('/user/subscription')
|
||||
}
|
||||
|
||||
logger.err({ error }, 'error loading upgrade subscription page')
|
||||
|
||||
return res.redirect('/user/subscription')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,11 @@ const PlansLocator = require('./PlansLocator')
|
||||
const SubscriptionHandler = require('./SubscriptionHandler')
|
||||
const GroupPlansData = require('./GroupPlansData')
|
||||
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./RecurlyEntities')
|
||||
const { ManuallyCollectedError, PendingChangeError } = require('./Errors')
|
||||
const {
|
||||
ManuallyCollectedError,
|
||||
PendingChangeError,
|
||||
InactiveError,
|
||||
} = require('./Errors')
|
||||
|
||||
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
|
||||
await SubscriptionUpdater.promises.removeUserFromGroup(
|
||||
@@ -65,7 +69,9 @@ async function ensureFlexibleLicensingEnabled(plan) {
|
||||
|
||||
async function ensureSubscriptionIsActive(subscription) {
|
||||
if (subscription?.recurlyStatus?.state !== 'active') {
|
||||
throw new Error('The subscription is not active')
|
||||
throw new InactiveError('The subscription is not active', {
|
||||
subscriptionId: subscription._id.toString(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+68
-28
@@ -27,11 +27,16 @@ import {
|
||||
AddOnUpdate,
|
||||
SubscriptionChangePreview,
|
||||
} from '../../../../../../types/subscription/subscription-change-preview'
|
||||
import { MergeAndOverride } from '../../../../../../types/utils'
|
||||
import { MergeAndOverride, Nullable } from '../../../../../../types/utils'
|
||||
import { sendMB } from '../../../../infrastructure/event-tracking'
|
||||
|
||||
export const MAX_NUMBER_OF_USERS = 50
|
||||
|
||||
type CostSummaryData = MergeAndOverride<
|
||||
SubscriptionChangePreview,
|
||||
{ change: AddOnUpdate }
|
||||
>
|
||||
|
||||
function AddSeats() {
|
||||
const { t } = useTranslation()
|
||||
const groupName = getMeta('ol-groupName')
|
||||
@@ -45,12 +50,11 @@ function AddSeats() {
|
||||
const { signal: contactSalesSignal } = useAbortController()
|
||||
const {
|
||||
isLoading: isLoadingCostSummary,
|
||||
isError: isErrorCostSummary,
|
||||
runAsync: runAsyncCostSummary,
|
||||
data: costSummaryData,
|
||||
reset: resetCostSummaryData,
|
||||
} = useAsync<
|
||||
MergeAndOverride<SubscriptionChangePreview, { change: AddOnUpdate }>
|
||||
>()
|
||||
} = useAsync<CostSummaryData>()
|
||||
const {
|
||||
isLoading: isAddingSeats,
|
||||
isError: isErrorAddingSeats,
|
||||
@@ -319,30 +323,13 @@ function AddSeats() {
|
||||
)}
|
||||
</FormGroup>
|
||||
</div>
|
||||
{isLoadingCostSummary ? (
|
||||
<LoadingSpinner className="ms-auto me-auto" />
|
||||
) : shouldContactSales ? (
|
||||
<div>
|
||||
<Notification
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="if_you_want_more_than_x_users_on_your_plan_we_need_to_add_them_for_you"
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
components={[<b />]}
|
||||
values={{ count: 50 }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
}
|
||||
type="info"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<CostSummary
|
||||
subscriptionChange={costSummaryData}
|
||||
totalLicenses={totalLicenses}
|
||||
/>
|
||||
)}
|
||||
<CostSummarySection
|
||||
isLoadingCostSummary={isLoadingCostSummary}
|
||||
isErrorCostSummary={isErrorCostSummary}
|
||||
shouldContactSales={shouldContactSales}
|
||||
costSummaryData={costSummaryData}
|
||||
totalLicenses={totalLicenses}
|
||||
/>
|
||||
<div className="d-flex align-items-center justify-content-end gap-2">
|
||||
{!isProfessional && (
|
||||
<a
|
||||
@@ -389,4 +376,57 @@ function AddSeats() {
|
||||
)
|
||||
}
|
||||
|
||||
type CostSummarySectionProps = {
|
||||
isLoadingCostSummary: boolean
|
||||
isErrorCostSummary: boolean
|
||||
shouldContactSales: boolean
|
||||
costSummaryData: Nullable<CostSummaryData>
|
||||
totalLicenses: number
|
||||
}
|
||||
|
||||
function CostSummarySection({
|
||||
isLoadingCostSummary,
|
||||
isErrorCostSummary,
|
||||
shouldContactSales,
|
||||
costSummaryData,
|
||||
totalLicenses,
|
||||
}: CostSummarySectionProps) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (isLoadingCostSummary) {
|
||||
return <LoadingSpinner className="ms-auto me-auto" />
|
||||
}
|
||||
|
||||
if (shouldContactSales) {
|
||||
return (
|
||||
<Notification
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="if_you_want_more_than_x_users_on_your_plan_we_need_to_add_them_for_you"
|
||||
// eslint-disable-next-line react/jsx-key
|
||||
components={[<b />]}
|
||||
values={{ count: 50 }}
|
||||
shouldUnescape
|
||||
tOptions={{ interpolation: { escapeValue: true } }}
|
||||
/>
|
||||
}
|
||||
type="info"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isErrorCostSummary) {
|
||||
return (
|
||||
<Notification type="error" content={t('generic_something_went_wrong')} />
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<CostSummary
|
||||
subscriptionChange={costSummaryData}
|
||||
totalLicenses={totalLicenses}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default withErrorBoundary(AddSeats)
|
||||
|
||||
@@ -110,7 +110,7 @@ describe('RecurlyClient', function () {
|
||||
},
|
||||
}
|
||||
this.Errors = {
|
||||
MissingBillingInfoError: class MissingBillingInfoError extends Error {},
|
||||
MissingBillingInfoError: class extends Error {},
|
||||
}
|
||||
|
||||
return (this.RecurlyClient = SandboxedModule.require(MODULE_PATH, {
|
||||
|
||||
@@ -124,9 +124,10 @@ describe('SubscriptionGroupController', function () {
|
||||
}
|
||||
|
||||
this.Errors = {
|
||||
MissingBillingInfoError: class MissingBillingInfoError extends Error {},
|
||||
ManuallyCollectedError: class ManuallyCollectedError extends Error {},
|
||||
PendingChangeError: class PendingChangeError extends Error {},
|
||||
MissingBillingInfoError: class extends Error {},
|
||||
ManuallyCollectedError: class extends Error {},
|
||||
PendingChangeError: class extends Error {},
|
||||
InactiveError: class extends Error {},
|
||||
}
|
||||
|
||||
this.Controller = await esmock.strict(modulePath, {
|
||||
@@ -482,7 +483,7 @@ describe('SubscriptionGroupController', function () {
|
||||
|
||||
const res = {
|
||||
status: statusCode => {
|
||||
statusCode.should.equal(400)
|
||||
statusCode.should.equal(500)
|
||||
|
||||
return {
|
||||
end: () => {
|
||||
@@ -519,7 +520,7 @@ describe('SubscriptionGroupController', function () {
|
||||
|
||||
const res = {
|
||||
status: statusCode => {
|
||||
statusCode.should.equal(400)
|
||||
statusCode.should.equal(500)
|
||||
|
||||
return {
|
||||
end: () => {
|
||||
|
||||
@@ -594,7 +594,7 @@ describe('SubscriptionGroupHandler', function () {
|
||||
describe('ensureSubscriptionIsActive', function () {
|
||||
it('should throw if the subscription is not active', async function () {
|
||||
await expect(
|
||||
this.Handler.promises.ensureSubscriptionIsActive({})
|
||||
this.Handler.promises.ensureSubscriptionIsActive(this.subscription)
|
||||
).to.be.rejectedWith('The subscription is not active')
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user