Merge pull request #23415 from overleaf/ii-flexible-group-licensing-manually-collected

[web] Manually collected subscriptions with flexible licensing

GitOrigin-RevId: ca7cf2abf5cfa1d873614bf3407fb5a259a78a76
This commit is contained in:
Liangjun Song
2025-02-07 09:05:28 +00:00
committed by Copybot
parent d66c11e786
commit 8421bcc5d2
17 changed files with 263 additions and 45 deletions
@@ -16,9 +16,12 @@ class AddOnNotPresentError extends OError {}
class MissingBillingInfoError extends OError {}
class ManuallyCollectedError extends OError {}
module.exports = {
RecurlyTransactionError,
DuplicateAddOnError,
AddOnNotPresentError,
MissingBillingInfoError,
ManuallyCollectedError,
}
@@ -261,7 +261,8 @@ function subscriptionFromApi(apiSubscription) {
apiSubscription.currency == null ||
apiSubscription.currentPeriodStartedAt == null ||
apiSubscription.currentPeriodEndsAt == null ||
apiSubscription.createdAt == null
apiSubscription.createdAt == null ||
apiSubscription.collectionMethod == null
) {
throw new OError('Invalid Recurly subscription', {
subscription: apiSubscription,
@@ -283,6 +284,7 @@ function subscriptionFromApi(apiSubscription) {
periodStart: apiSubscription.currentPeriodStartedAt,
periodEnd: apiSubscription.currentPeriodEndsAt,
createdAt: apiSubscription.createdAt,
collectionMethod: apiSubscription.collectionMethod,
})
if (apiSubscription.pendingChange != null) {
@@ -26,6 +26,7 @@ class RecurlySubscription {
* @param {Date} props.periodStart
* @param {Date} props.periodEnd
* @param {Date} props.createdAt
* @param {string} props.collectionMethod
* @param {RecurlySubscriptionChange} [props.pendingChange]
*/
constructor(props) {
@@ -43,6 +44,7 @@ class RecurlySubscription {
this.periodStart = props.periodStart
this.periodEnd = props.periodEnd
this.createdAt = props.createdAt
this.collectionMethod = props.collectionMethod
this.pendingChange = props.pendingChange ?? null
}
@@ -246,6 +248,15 @@ class RecurlySubscription {
planCode: newPlanCode,
})
}
/**
* Returns whether this subscription is manually collected
*
* @return {boolean}
*/
get isCollectionMethodManual() {
return this.collectionMethod === 'manual'
}
}
/**
@@ -13,7 +13,7 @@ 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 } from './Errors.js'
import { MissingBillingInfoError, ManuallyCollectedError } from './Errors.js'
import RecurlyClient from './RecurlyClient.js'
/**
@@ -126,11 +126,14 @@ async function _removeUserFromGroup(
async function addSeatsToGroupSubscription(req, res) {
try {
const userId = SessionManager.getLoggedInUserId(req.session)
const { subscription, plan } =
const { subscription, recurlySubscription, plan } =
await SubscriptionGroupHandler.promises.getUsersGroupSubscriptionDetails(
userId
)
await SubscriptionGroupHandler.promises.ensureFlexibleLicensingEnabled(plan)
await SubscriptionGroupHandler.promises.ensureSubscriptionCollectionMethodIsNotManual(
recurlySubscription
)
// Check if the user has missing billing details
await RecurlyClient.promises.getPaymentMethod(userId)
await SubscriptionGroupHandler.promises.ensureSubscriptionIsActive(
@@ -155,6 +158,12 @@ async function addSeatsToGroupSubscription(req, res) {
)
}
if (error instanceof ManuallyCollectedError) {
return res.redirect(
'/user/subscription/group/manually-collected-subscription'
)
}
return res.redirect('/user/subscription')
}
}
@@ -268,6 +277,12 @@ async function subscriptionUpgradePage(req, res) {
)
}
if (error instanceof ManuallyCollectedError) {
return res.redirect(
'/user/subscription/group/manually-collected-subscription'
)
}
return res.redirect('/user/subscription')
}
}
@@ -301,6 +316,24 @@ async function missingBillingInformation(req, res) {
}
}
async function manuallyCollectedSubscription(req, res) {
try {
const userId = SessionManager.getLoggedInUserId(req.session)
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
res.render('subscriptions/manually-collected-subscription', {
groupName: subscription.teamName,
})
} catch (error) {
logger.err(
{ error },
'error trying to render manually collected subscription page'
)
return res.render('/user/subscription')
}
}
export default {
removeUserFromGroup: expressify(removeUserFromGroup),
removeSelfFromGroup: expressify(removeSelfFromGroup),
@@ -316,4 +349,5 @@ export default {
subscriptionUpgradePage: expressify(subscriptionUpgradePage),
upgradeSubscription: expressify(upgradeSubscription),
missingBillingInformation: expressify(missingBillingInformation),
manuallyCollectedSubscription: expressify(manuallyCollectedSubscription),
}
@@ -8,6 +8,7 @@ const PlansLocator = require('./PlansLocator')
const SubscriptionHandler = require('./SubscriptionHandler')
const GroupPlansData = require('./GroupPlansData')
const { MEMBERS_LIMIT_ADD_ON_CODE } = require('./RecurlyEntities')
const { ManuallyCollectedError } = require('./Errors')
async function removeUserFromGroup(subscriptionId, userIdToRemove) {
await SubscriptionUpdater.promises.removeUserFromGroup(
@@ -68,6 +69,19 @@ async function ensureSubscriptionIsActive(subscription) {
}
}
async function ensureSubscriptionCollectionMethodIsNotManual(
recurlySubscription
) {
if (recurlySubscription.isCollectionMethodManual) {
throw new ManuallyCollectedError(
'This subscription is being collected manually',
{
recurlySubscription_id: recurlySubscription.id,
}
)
}
}
async function getUsersGroupSubscriptionDetails(userId) {
const subscription =
await SubscriptionLocator.promises.getUsersSubscription(userId)
@@ -99,6 +113,8 @@ async function _addSeatsSubscriptionChange(userId, adding) {
await getUsersGroupSubscriptionDetails(userId)
await ensureFlexibleLicensingEnabled(plan)
await ensureSubscriptionIsActive(subscription)
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
const currentAddonQuantity =
recurlySubscription.addOns.find(
addOn => addOn.code === MEMBERS_LIMIT_ADD_ON_CODE
@@ -207,6 +223,8 @@ async function _getGroupPlanUpgradeChangeRequest(ownerId) {
olSubscription.recurlySubscription_id
)
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
return recurlySubscription.getRequestForGroupPlanUpgrade(newPlanCode)
}
@@ -244,6 +262,9 @@ module.exports = {
replaceUserReferencesInGroups: callbackify(replaceUserReferencesInGroups),
ensureFlexibleLicensingEnabled: callbackify(ensureFlexibleLicensingEnabled),
ensureSubscriptionIsActive: callbackify(ensureSubscriptionIsActive),
ensureSubscriptionCollectionMethodIsNotManual: callbackify(
ensureSubscriptionCollectionMethodIsNotManual
),
getTotalConfirmedUsersInGroup: callbackify(getTotalConfirmedUsersInGroup),
isUserPartOfGroup: callbackify(isUserPartOfGroup),
getGroupPlanUpgradePreview: callbackify(getGroupPlanUpgradePreview),
@@ -253,6 +274,7 @@ module.exports = {
replaceUserReferencesInGroups,
ensureFlexibleLicensingEnabled,
ensureSubscriptionIsActive,
ensureSubscriptionCollectionMethodIsNotManual,
getTotalConfirmedUsersInGroup,
isUserPartOfGroup,
getUsersGroupSubscriptionDetails,
@@ -127,6 +127,14 @@ export default {
SubscriptionGroupController.missingBillingInformation
)
webRouter.get(
'/user/subscription/group/manually-collected-subscription',
AuthenticationController.requireLogin(),
RateLimiterMiddleware.rateLimit(subscriptionRateLimiter),
SubscriptionGroupController.flexibleLicensingSplitTest,
SubscriptionGroupController.manuallyCollectedSubscription
)
// Team invites
webRouter.get(
'/subscription/invites/:token/',