Merge pull request #26519 from overleaf/ls-group-plan-upgrade-in-stripe

Support group plan upgrade in Stripe

GitOrigin-RevId: 44ae35c6221bf96a0b91526178d6ab1aff80f2a3
This commit is contained in:
Liangjun Song
2025-06-25 08:06:54 +00:00
committed by Copybot
parent 903277c222
commit c7ae851d39
3 changed files with 33 additions and 127 deletions
@@ -296,39 +296,12 @@ async function updateSubscriptionPaymentTerms(
await RecurlyClient.promises.updateSubscriptionDetails(updateRequest)
}
async function _getUpgradeTargetPlanCodeMaybeThrow(subscription) {
if (
subscription.planCode.includes('professional') ||
!subscription.groupPlan
) {
throw new Error('Not eligible for group plan upgrade')
}
return subscription.planCode.replace('collaborator', 'professional')
}
async function _getGroupPlanUpgradeChangeRequest(ownerId) {
const olSubscription =
await SubscriptionLocator.promises.getUsersSubscription(ownerId)
await ensureSubscriptionIsActive(olSubscription)
const newPlanCode = await _getUpgradeTargetPlanCodeMaybeThrow(olSubscription)
const recurlySubscription = await RecurlyClient.promises.getSubscription(
olSubscription.recurlySubscription_id
)
await ensureSubscriptionCollectionMethodIsNotManual(recurlySubscription)
await ensureSubscriptionHasNoPendingChanges(recurlySubscription)
return recurlySubscription.getRequestForGroupPlanUpgrade(newPlanCode)
}
async function getGroupPlanUpgradePreview(ownerId) {
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
const subscriptionChange =
await RecurlyClient.promises.previewSubscriptionChange(changeRequest)
const paymentMethod = await RecurlyClient.promises.getPaymentMethod(ownerId)
const preview = await Modules.promises.hooks.fire(
'previewGroupPlanUpgrade',
ownerId
)
const { subscriptionChange, paymentMethod } = preview[0]
return SubscriptionController.makeChangePreview(
{
type: 'group-plan-upgrade',
@@ -345,12 +318,7 @@ async function getGroupPlanUpgradePreview(ownerId) {
}
async function upgradeGroupPlan(ownerId) {
const changeRequest = await _getGroupPlanUpgradeChangeRequest(ownerId)
await RecurlyClient.promises.applySubscriptionChangeRequest(changeRequest)
await SubscriptionHandler.promises.syncSubscription(
{ uuid: changeRequest.subscription.id },
ownerId
)
await Modules.promises.hooks.fire('upgradeGroupPlan', ownerId)
}
async function updateGroupMembersBulk(
@@ -199,6 +199,30 @@ module.exports = {
price_in_cents: 3000,
features: features.professional,
},
{
planCode: 'group_professional',
name: 'Professional - Group Account - Enterprise',
hideFromUsers: true,
price_in_cents: 0,
annual: true,
features: features.professional,
groupPlan: true,
membersLimit: 0,
membersLimitAddOn: 'additional-license',
canUseFlexibleLicensing: true,
},
{
planCode: 'group_collaborator',
name: 'Collaborator - Group Account - Enterprise',
hideFromUsers: true,
price_in_cents: 0,
annual: true,
features: features.collaborator,
groupPlan: true,
membersLimit: 0,
membersLimitAddOn: 'additional-license',
canUseFlexibleLicensing: true,
},
],
bonus_features: {
@@ -779,100 +779,14 @@ describe('SubscriptionGroupHandler', function () {
})
})
describe('upgradeGroupPlan', function () {
it('should upgrade the subscription for flexible licensing group plans', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: true,
recurlyStatus: {
state: 'active',
},
planCode: 'group_collaborator',
})
await this.Handler.promises.upgradeGroupPlan(this.user_id)
this.recurlySubscription.getRequestForGroupPlanUpgrade
.calledWith('group_professional')
.should.equal(true)
this.RecurlyClient.promises.applySubscriptionChangeRequest
.calledWith(this.changeRequest)
.should.equal(true)
this.SubscriptionHandler.promises.syncSubscription
.calledWith({ uuid: this.changeRequest.subscription.id }, this.user_id)
.should.equal(true)
})
it('should upgrade the subscription for legacy group plans', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: true,
recurlyStatus: {
state: 'active',
},
planCode: 'group_collaborator_10_educational',
})
await this.Handler.promises.upgradeGroupPlan(this.user_id)
this.recurlySubscription.getRequestForGroupPlanUpgrade
.calledWith('group_professional_10_educational')
.should.equal(true)
this.RecurlyClient.promises.applySubscriptionChangeRequest
.calledWith(this.changeRequest)
.should.equal(true)
this.SubscriptionHandler.promises.syncSubscription
.calledWith({ uuid: this.changeRequest.subscription.id }, this.user_id)
.should.equal(true)
})
it('should fail the upgrade if is professional already', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: true,
recurlyStatus: {
state: 'active',
},
planCode: 'group_professional',
})
await expect(
this.Handler.promises.upgradeGroupPlan(this.user_id)
).to.be.rejectedWith('Not eligible for group plan upgrade')
})
it('should fail the upgrade if not group plan', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: false,
recurlyStatus: {
state: 'active',
},
planCode: 'test_plan_code',
})
await expect(
this.Handler.promises.upgradeGroupPlan(this.user_id)
).to.be.rejectedWith('Not eligible for group plan upgrade')
})
})
describe('getGroupPlanUpgradePreview', function () {
it('should generate preview for subscription upgrade', async function () {
this.SubscriptionLocator.promises.getUsersSubscription = sinon
.stub()
.resolves({
groupPlan: true,
recurlyStatus: {
state: 'active',
},
planCode: 'group_collaborator',
})
this.Modules.promises.hooks.fire.resolves([
{ subscriptionChange: this.previewSubscriptionChange },
])
const result = await this.Handler.promises.getGroupPlanUpgradePreview(
this.user_id
)
this.RecurlyClient.promises.previewSubscriptionChange
.calledWith(this.changeRequest)
.should.equal(true)
result.should.equal(this.changePreview)
})
})